edsl 0.1.47__py3-none-any.whl → 0.1.49__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 (314) hide show
  1. edsl/__init__.py +44 -39
  2. edsl/__version__.py +1 -1
  3. edsl/agents/__init__.py +4 -2
  4. edsl/agents/{Agent.py → agent.py} +442 -152
  5. edsl/agents/{AgentList.py → agent_list.py} +220 -162
  6. edsl/agents/descriptors.py +46 -7
  7. edsl/{exceptions/agents.py → agents/exceptions.py} +3 -12
  8. edsl/base/__init__.py +75 -0
  9. edsl/base/base_class.py +1303 -0
  10. edsl/base/data_transfer_models.py +114 -0
  11. edsl/base/enums.py +215 -0
  12. edsl/base.py +8 -0
  13. edsl/buckets/__init__.py +25 -0
  14. edsl/buckets/bucket_collection.py +324 -0
  15. edsl/buckets/model_buckets.py +206 -0
  16. edsl/buckets/token_bucket.py +502 -0
  17. edsl/{jobs/buckets/TokenBucketAPI.py → buckets/token_bucket_api.py} +1 -1
  18. edsl/buckets/token_bucket_client.py +509 -0
  19. edsl/caching/__init__.py +20 -0
  20. edsl/caching/cache.py +814 -0
  21. edsl/caching/cache_entry.py +427 -0
  22. edsl/{data/CacheHandler.py → caching/cache_handler.py} +14 -15
  23. edsl/caching/exceptions.py +24 -0
  24. edsl/caching/orm.py +30 -0
  25. edsl/{data/RemoteCacheSync.py → caching/remote_cache_sync.py} +3 -3
  26. edsl/caching/sql_dict.py +441 -0
  27. edsl/config/__init__.py +8 -0
  28. edsl/config/config_class.py +177 -0
  29. edsl/config.py +4 -176
  30. edsl/conversation/Conversation.py +7 -7
  31. edsl/conversation/car_buying.py +4 -4
  32. edsl/conversation/chips.py +6 -6
  33. edsl/coop/__init__.py +25 -2
  34. edsl/coop/coop.py +311 -75
  35. edsl/coop/{ExpectedParrotKeyHandler.py → ep_key_handling.py} +86 -10
  36. edsl/coop/exceptions.py +62 -0
  37. edsl/coop/price_fetcher.py +126 -0
  38. edsl/coop/utils.py +89 -24
  39. edsl/data_transfer_models.py +5 -72
  40. edsl/dataset/__init__.py +10 -0
  41. edsl/{results/Dataset.py → dataset/dataset.py} +116 -36
  42. edsl/{results/DatasetExportMixin.py → dataset/dataset_operations_mixin.py} +606 -122
  43. edsl/{results/DatasetTree.py → dataset/dataset_tree.py} +156 -75
  44. edsl/{results/TableDisplay.py → dataset/display/table_display.py} +18 -7
  45. edsl/{results → dataset/display}/table_renderers.py +58 -2
  46. edsl/{results → dataset}/file_exports.py +4 -5
  47. edsl/{results → dataset}/smart_objects.py +2 -2
  48. edsl/enums.py +5 -205
  49. edsl/inference_services/__init__.py +5 -0
  50. edsl/inference_services/{AvailableModelCacheHandler.py → available_model_cache_handler.py} +2 -3
  51. edsl/inference_services/{AvailableModelFetcher.py → available_model_fetcher.py} +8 -14
  52. edsl/inference_services/data_structures.py +3 -2
  53. edsl/{exceptions/inference_services.py → inference_services/exceptions.py} +1 -1
  54. edsl/inference_services/{InferenceServiceABC.py → inference_service_abc.py} +1 -1
  55. edsl/inference_services/{InferenceServicesCollection.py → inference_services_collection.py} +8 -7
  56. edsl/inference_services/registry.py +4 -41
  57. edsl/inference_services/{ServiceAvailability.py → service_availability.py} +5 -25
  58. edsl/inference_services/services/__init__.py +31 -0
  59. edsl/inference_services/{AnthropicService.py → services/anthropic_service.py} +3 -3
  60. edsl/inference_services/{AwsBedrock.py → services/aws_bedrock.py} +2 -2
  61. edsl/inference_services/{AzureAI.py → services/azure_ai.py} +2 -2
  62. edsl/inference_services/{DeepInfraService.py → services/deep_infra_service.py} +1 -3
  63. edsl/inference_services/{DeepSeekService.py → services/deep_seek_service.py} +2 -4
  64. edsl/inference_services/{GoogleService.py → services/google_service.py} +5 -4
  65. edsl/inference_services/{GroqService.py → services/groq_service.py} +1 -1
  66. edsl/inference_services/{MistralAIService.py → services/mistral_ai_service.py} +3 -3
  67. edsl/inference_services/{OllamaService.py → services/ollama_service.py} +1 -7
  68. edsl/inference_services/{OpenAIService.py → services/open_ai_service.py} +5 -6
  69. edsl/inference_services/{PerplexityService.py → services/perplexity_service.py} +3 -7
  70. edsl/inference_services/{TestService.py → services/test_service.py} +7 -6
  71. edsl/inference_services/{TogetherAIService.py → services/together_ai_service.py} +2 -6
  72. edsl/inference_services/{XAIService.py → services/xai_service.py} +1 -1
  73. edsl/inference_services/write_available.py +1 -2
  74. edsl/instructions/__init__.py +6 -0
  75. edsl/{surveys/instructions/Instruction.py → instructions/instruction.py} +11 -6
  76. edsl/{surveys/instructions/InstructionCollection.py → instructions/instruction_collection.py} +10 -5
  77. edsl/{surveys/InstructionHandler.py → instructions/instruction_handler.py} +3 -3
  78. edsl/{jobs/interviews → interviews}/ReportErrors.py +2 -2
  79. edsl/interviews/__init__.py +4 -0
  80. edsl/{jobs/AnswerQuestionFunctionConstructor.py → interviews/answering_function.py} +45 -18
  81. edsl/{jobs/interviews/InterviewExceptionEntry.py → interviews/exception_tracking.py} +107 -22
  82. edsl/interviews/interview.py +638 -0
  83. edsl/{jobs/interviews/InterviewStatusDictionary.py → interviews/interview_status_dictionary.py} +21 -12
  84. edsl/{jobs/interviews/InterviewStatusLog.py → interviews/interview_status_log.py} +16 -7
  85. edsl/{jobs/InterviewTaskManager.py → interviews/interview_task_manager.py} +12 -7
  86. edsl/{jobs/RequestTokenEstimator.py → interviews/request_token_estimator.py} +8 -3
  87. edsl/{jobs/interviews/InterviewStatistic.py → interviews/statistics.py} +36 -10
  88. edsl/invigilators/__init__.py +38 -0
  89. edsl/invigilators/invigilator_base.py +477 -0
  90. edsl/{agents/Invigilator.py → invigilators/invigilators.py} +263 -10
  91. edsl/invigilators/prompt_constructor.py +476 -0
  92. edsl/{agents → invigilators}/prompt_helpers.py +2 -1
  93. edsl/{agents/QuestionInstructionPromptBuilder.py → invigilators/question_instructions_prompt_builder.py} +18 -13
  94. edsl/{agents → invigilators}/question_option_processor.py +96 -21
  95. edsl/{agents/QuestionTemplateReplacementsBuilder.py → invigilators/question_template_replacements_builder.py} +64 -12
  96. edsl/jobs/__init__.py +7 -1
  97. edsl/jobs/async_interview_runner.py +99 -35
  98. edsl/jobs/check_survey_scenario_compatibility.py +7 -5
  99. edsl/jobs/data_structures.py +153 -22
  100. edsl/{exceptions/jobs.py → jobs/exceptions.py} +2 -1
  101. edsl/jobs/{FetchInvigilator.py → fetch_invigilator.py} +4 -4
  102. edsl/jobs/{loggers/HTMLTableJobLogger.py → html_table_job_logger.py} +6 -2
  103. edsl/jobs/{Jobs.py → jobs.py} +313 -167
  104. edsl/jobs/{JobsChecks.py → jobs_checks.py} +15 -7
  105. edsl/jobs/{JobsComponentConstructor.py → jobs_component_constructor.py} +19 -17
  106. edsl/jobs/{InterviewsConstructor.py → jobs_interview_constructor.py} +10 -5
  107. edsl/jobs/jobs_pricing_estimation.py +347 -0
  108. edsl/jobs/{JobsRemoteInferenceLogger.py → jobs_remote_inference_logger.py} +4 -3
  109. edsl/jobs/jobs_runner_asyncio.py +282 -0
  110. edsl/jobs/{JobsRemoteInferenceHandler.py → remote_inference.py} +19 -22
  111. edsl/jobs/results_exceptions_handler.py +2 -2
  112. edsl/key_management/__init__.py +28 -0
  113. edsl/key_management/key_lookup.py +161 -0
  114. edsl/{language_models/key_management/KeyLookupBuilder.py → key_management/key_lookup_builder.py} +118 -47
  115. edsl/key_management/key_lookup_collection.py +82 -0
  116. edsl/key_management/models.py +218 -0
  117. edsl/language_models/__init__.py +7 -2
  118. edsl/language_models/{ComputeCost.py → compute_cost.py} +18 -3
  119. edsl/{exceptions/language_models.py → language_models/exceptions.py} +2 -1
  120. edsl/language_models/language_model.py +1080 -0
  121. edsl/language_models/model.py +10 -25
  122. edsl/language_models/{ModelList.py → model_list.py} +9 -14
  123. edsl/language_models/{RawResponseHandler.py → raw_response_handler.py} +1 -1
  124. edsl/language_models/{RegisterLanguageModelsMeta.py → registry.py} +1 -1
  125. edsl/language_models/repair.py +4 -4
  126. edsl/language_models/utilities.py +4 -4
  127. edsl/notebooks/__init__.py +3 -1
  128. edsl/notebooks/{Notebook.py → notebook.py} +7 -8
  129. edsl/prompts/__init__.py +1 -1
  130. edsl/{exceptions/prompts.py → prompts/exceptions.py} +3 -1
  131. edsl/prompts/{Prompt.py → prompt.py} +101 -95
  132. edsl/questions/HTMLQuestion.py +1 -1
  133. edsl/questions/__init__.py +154 -25
  134. edsl/questions/answer_validator_mixin.py +1 -1
  135. edsl/questions/compose_questions.py +4 -3
  136. edsl/questions/derived/question_likert_five.py +166 -0
  137. edsl/questions/derived/{QuestionLinearScale.py → question_linear_scale.py} +4 -4
  138. edsl/questions/derived/{QuestionTopK.py → question_top_k.py} +4 -4
  139. edsl/questions/derived/{QuestionYesNo.py → question_yes_no.py} +4 -5
  140. edsl/questions/descriptors.py +24 -30
  141. edsl/questions/loop_processor.py +65 -19
  142. edsl/questions/question_base.py +881 -0
  143. edsl/questions/question_base_gen_mixin.py +15 -16
  144. edsl/questions/{QuestionBasePromptsMixin.py → question_base_prompts_mixin.py} +2 -2
  145. edsl/questions/{QuestionBudget.py → question_budget.py} +3 -4
  146. edsl/questions/{QuestionCheckBox.py → question_check_box.py} +16 -16
  147. edsl/questions/{QuestionDict.py → question_dict.py} +39 -5
  148. edsl/questions/{QuestionExtract.py → question_extract.py} +9 -9
  149. edsl/questions/question_free_text.py +282 -0
  150. edsl/questions/{QuestionFunctional.py → question_functional.py} +6 -5
  151. edsl/questions/{QuestionList.py → question_list.py} +6 -7
  152. edsl/questions/{QuestionMatrix.py → question_matrix.py} +6 -5
  153. edsl/questions/{QuestionMultipleChoice.py → question_multiple_choice.py} +126 -21
  154. edsl/questions/{QuestionNumerical.py → question_numerical.py} +5 -5
  155. edsl/questions/{QuestionRank.py → question_rank.py} +6 -6
  156. edsl/questions/question_registry.py +4 -9
  157. edsl/questions/register_questions_meta.py +8 -4
  158. edsl/questions/response_validator_abc.py +17 -16
  159. edsl/results/__init__.py +4 -1
  160. edsl/{exceptions/results.py → results/exceptions.py} +1 -1
  161. edsl/results/report.py +197 -0
  162. edsl/results/{Result.py → result.py} +131 -45
  163. edsl/results/{Results.py → results.py} +365 -220
  164. edsl/results/results_selector.py +344 -25
  165. edsl/scenarios/__init__.py +30 -3
  166. edsl/scenarios/{ConstructDownloadLink.py → construct_download_link.py} +7 -0
  167. edsl/scenarios/directory_scanner.py +156 -13
  168. edsl/scenarios/document_chunker.py +186 -0
  169. edsl/scenarios/exceptions.py +101 -0
  170. edsl/scenarios/file_methods.py +2 -3
  171. edsl/scenarios/{FileStore.py → file_store.py} +275 -189
  172. edsl/scenarios/handlers/__init__.py +14 -14
  173. edsl/scenarios/handlers/{csv.py → csv_file_store.py} +1 -2
  174. edsl/scenarios/handlers/{docx.py → docx_file_store.py} +8 -7
  175. edsl/scenarios/handlers/{html.py → html_file_store.py} +1 -2
  176. edsl/scenarios/handlers/{jpeg.py → jpeg_file_store.py} +1 -1
  177. edsl/scenarios/handlers/{json.py → json_file_store.py} +1 -1
  178. edsl/scenarios/handlers/latex_file_store.py +5 -0
  179. edsl/scenarios/handlers/{md.py → md_file_store.py} +1 -1
  180. edsl/scenarios/handlers/{pdf.py → pdf_file_store.py} +2 -2
  181. edsl/scenarios/handlers/{png.py → png_file_store.py} +1 -1
  182. edsl/scenarios/handlers/{pptx.py → pptx_file_store.py} +8 -7
  183. edsl/scenarios/handlers/{py.py → py_file_store.py} +1 -3
  184. edsl/scenarios/handlers/{sql.py → sql_file_store.py} +2 -1
  185. edsl/scenarios/handlers/{sqlite.py → sqlite_file_store.py} +2 -3
  186. edsl/scenarios/handlers/{txt.py → txt_file_store.py} +1 -1
  187. edsl/scenarios/scenario.py +928 -0
  188. edsl/scenarios/scenario_join.py +18 -5
  189. edsl/scenarios/{ScenarioList.py → scenario_list.py} +294 -106
  190. edsl/scenarios/{ScenarioListPdfMixin.py → scenario_list_pdf_tools.py} +16 -15
  191. edsl/scenarios/scenario_selector.py +5 -1
  192. edsl/study/ObjectEntry.py +2 -2
  193. edsl/study/SnapShot.py +5 -5
  194. edsl/study/Study.py +18 -19
  195. edsl/study/__init__.py +6 -4
  196. edsl/surveys/__init__.py +7 -4
  197. edsl/surveys/dag/__init__.py +2 -0
  198. edsl/surveys/{ConstructDAG.py → dag/construct_dag.py} +3 -3
  199. edsl/surveys/{DAG.py → dag/dag.py} +13 -10
  200. edsl/surveys/descriptors.py +1 -1
  201. edsl/surveys/{EditSurvey.py → edit_survey.py} +9 -9
  202. edsl/{exceptions/surveys.py → surveys/exceptions.py} +1 -2
  203. edsl/surveys/memory/__init__.py +3 -0
  204. edsl/surveys/{MemoryPlan.py → memory/memory_plan.py} +10 -9
  205. edsl/surveys/rules/__init__.py +3 -0
  206. edsl/surveys/{Rule.py → rules/rule.py} +103 -43
  207. edsl/surveys/{RuleCollection.py → rules/rule_collection.py} +21 -30
  208. edsl/surveys/{RuleManager.py → rules/rule_manager.py} +19 -13
  209. edsl/surveys/survey.py +1743 -0
  210. edsl/surveys/{SurveyExportMixin.py → survey_export.py} +22 -27
  211. edsl/surveys/{SurveyFlowVisualization.py → survey_flow_visualization.py} +11 -2
  212. edsl/surveys/{Simulator.py → survey_simulator.py} +10 -3
  213. edsl/tasks/__init__.py +32 -0
  214. edsl/{jobs/tasks/QuestionTaskCreator.py → tasks/question_task_creator.py} +115 -57
  215. edsl/tasks/task_creators.py +135 -0
  216. edsl/{jobs/tasks/TaskHistory.py → tasks/task_history.py} +86 -47
  217. edsl/{jobs/tasks → tasks}/task_status_enum.py +91 -7
  218. edsl/tasks/task_status_log.py +85 -0
  219. edsl/tokens/__init__.py +2 -0
  220. edsl/tokens/interview_token_usage.py +53 -0
  221. edsl/utilities/PrettyList.py +1 -1
  222. edsl/utilities/SystemInfo.py +25 -22
  223. edsl/utilities/__init__.py +29 -21
  224. edsl/utilities/gcp_bucket/__init__.py +2 -0
  225. edsl/utilities/gcp_bucket/cloud_storage.py +99 -96
  226. edsl/utilities/interface.py +44 -536
  227. edsl/{results/MarkdownToPDF.py → utilities/markdown_to_pdf.py} +13 -5
  228. edsl/utilities/repair_functions.py +1 -1
  229. {edsl-0.1.47.dist-info → edsl-0.1.49.dist-info}/METADATA +1 -1
  230. edsl-0.1.49.dist-info/RECORD +347 -0
  231. edsl/Base.py +0 -493
  232. edsl/BaseDiff.py +0 -260
  233. edsl/agents/InvigilatorBase.py +0 -260
  234. edsl/agents/PromptConstructor.py +0 -318
  235. edsl/coop/PriceFetcher.py +0 -54
  236. edsl/data/Cache.py +0 -582
  237. edsl/data/CacheEntry.py +0 -238
  238. edsl/data/SQLiteDict.py +0 -292
  239. edsl/data/__init__.py +0 -5
  240. edsl/data/orm.py +0 -10
  241. edsl/exceptions/cache.py +0 -5
  242. edsl/exceptions/coop.py +0 -14
  243. edsl/exceptions/data.py +0 -14
  244. edsl/exceptions/scenarios.py +0 -29
  245. edsl/jobs/Answers.py +0 -43
  246. edsl/jobs/JobsPrompts.py +0 -354
  247. edsl/jobs/buckets/BucketCollection.py +0 -134
  248. edsl/jobs/buckets/ModelBuckets.py +0 -65
  249. edsl/jobs/buckets/TokenBucket.py +0 -283
  250. edsl/jobs/buckets/TokenBucketClient.py +0 -191
  251. edsl/jobs/interviews/Interview.py +0 -395
  252. edsl/jobs/interviews/InterviewExceptionCollection.py +0 -99
  253. edsl/jobs/interviews/InterviewStatisticsCollection.py +0 -25
  254. edsl/jobs/runners/JobsRunnerAsyncio.py +0 -163
  255. edsl/jobs/runners/JobsRunnerStatusData.py +0 -0
  256. edsl/jobs/tasks/TaskCreators.py +0 -64
  257. edsl/jobs/tasks/TaskStatusLog.py +0 -23
  258. edsl/jobs/tokens/InterviewTokenUsage.py +0 -27
  259. edsl/language_models/LanguageModel.py +0 -635
  260. edsl/language_models/ServiceDataSources.py +0 -0
  261. edsl/language_models/key_management/KeyLookup.py +0 -63
  262. edsl/language_models/key_management/KeyLookupCollection.py +0 -38
  263. edsl/language_models/key_management/models.py +0 -137
  264. edsl/questions/QuestionBase.py +0 -544
  265. edsl/questions/QuestionFreeText.py +0 -130
  266. edsl/questions/derived/QuestionLikertFive.py +0 -76
  267. edsl/results/ResultsExportMixin.py +0 -45
  268. edsl/results/TextEditor.py +0 -50
  269. edsl/results/results_fetch_mixin.py +0 -33
  270. edsl/results/results_tools_mixin.py +0 -98
  271. edsl/scenarios/DocumentChunker.py +0 -104
  272. edsl/scenarios/Scenario.py +0 -548
  273. edsl/scenarios/ScenarioHtmlMixin.py +0 -65
  274. edsl/scenarios/ScenarioListExportMixin.py +0 -45
  275. edsl/scenarios/handlers/latex.py +0 -5
  276. edsl/shared.py +0 -1
  277. edsl/surveys/Survey.py +0 -1301
  278. edsl/surveys/SurveyQualtricsImport.py +0 -284
  279. edsl/surveys/SurveyToApp.py +0 -141
  280. edsl/surveys/instructions/__init__.py +0 -0
  281. edsl/tools/__init__.py +0 -1
  282. edsl/tools/clusters.py +0 -192
  283. edsl/tools/embeddings.py +0 -27
  284. edsl/tools/embeddings_plotting.py +0 -118
  285. edsl/tools/plotting.py +0 -112
  286. edsl/tools/summarize.py +0 -18
  287. edsl/utilities/data/Registry.py +0 -6
  288. edsl/utilities/data/__init__.py +0 -1
  289. edsl/utilities/data/scooter_results.json +0 -1
  290. edsl-0.1.47.dist-info/RECORD +0 -354
  291. /edsl/coop/{CoopFunctionsMixin.py → coop_functions.py} +0 -0
  292. /edsl/{results → dataset/display}/CSSParameterizer.py +0 -0
  293. /edsl/{language_models/key_management → dataset/display}/__init__.py +0 -0
  294. /edsl/{results → dataset/display}/table_data_class.py +0 -0
  295. /edsl/{results → dataset/display}/table_display.css +0 -0
  296. /edsl/{results/ResultsGGMixin.py → dataset/r/ggplot.py} +0 -0
  297. /edsl/{results → dataset}/tree_explore.py +0 -0
  298. /edsl/{surveys/instructions/ChangeInstruction.py → instructions/change_instruction.py} +0 -0
  299. /edsl/{jobs/interviews → interviews}/interview_status_enum.py +0 -0
  300. /edsl/jobs/{runners/JobsRunnerStatus.py → jobs_runner_status.py} +0 -0
  301. /edsl/language_models/{PriceManager.py → price_manager.py} +0 -0
  302. /edsl/language_models/{fake_openai_call.py → unused/fake_openai_call.py} +0 -0
  303. /edsl/language_models/{fake_openai_service.py → unused/fake_openai_service.py} +0 -0
  304. /edsl/notebooks/{NotebookToLaTeX.py → notebook_to_latex.py} +0 -0
  305. /edsl/{exceptions/questions.py → questions/exceptions.py} +0 -0
  306. /edsl/questions/{SimpleAskMixin.py → simple_ask_mixin.py} +0 -0
  307. /edsl/surveys/{Memory.py → memory/memory.py} +0 -0
  308. /edsl/surveys/{MemoryManagement.py → memory/memory_management.py} +0 -0
  309. /edsl/surveys/{SurveyCSS.py → survey_css.py} +0 -0
  310. /edsl/{jobs/tokens/TokenUsage.py → tokens/token_usage.py} +0 -0
  311. /edsl/{results/MarkdownToDocx.py → utilities/markdown_to_docx.py} +0 -0
  312. /edsl/{TemplateLoader.py → utilities/template_loader.py} +0 -0
  313. {edsl-0.1.47.dist-info → edsl-0.1.49.dist-info}/LICENSE +0 -0
  314. {edsl-0.1.47.dist-info → edsl-0.1.49.dist-info}/WHEEL +0 -0
@@ -0,0 +1,881 @@
1
+ """
2
+ Base module for all question types in the EDSL framework.
3
+
4
+ The question_base module defines the QuestionBase abstract base class, which serves as
5
+ the foundation for all question types in EDSL. This module establishes the core
6
+ functionality, interface, and behavior that all questions must implement.
7
+
8
+ Key features of this module include:
9
+ - Abstract base class that defines the question interface
10
+ - Core validation and serialization capabilities
11
+ - Integration with language models and agents
12
+ - Support for template-based question generation
13
+ - Connection to response validation and answer processing
14
+
15
+ This module is one of the most important in EDSL as it establishes the contract that
16
+ all question types must follow, enabling consistent behavior across different types
17
+ of questions while allowing for specialized functionality in derived classes.
18
+
19
+ Technical Details:
20
+ -----------------
21
+ 1. Question Architecture:
22
+ - QuestionBase is an abstract base class that cannot be instantiated directly
23
+ - It uses multiple inheritance from several mixins to provide different capabilities
24
+ - The RegisterQuestionsMeta metaclass enables automatic registration of question types
25
+ - Each concrete question type must define specific class attributes and methods
26
+
27
+ 2. Inheritance Hierarchy:
28
+ - PersistenceMixin: Provides serialization and deserialization via to_dict/from_dict
29
+ - RepresentationMixin: Provides string representation via __repr__
30
+ - SimpleAskMixin: Provides the basic asking functionality to interact with models
31
+ - QuestionBasePromptsMixin: Handles template-based prompt generation
32
+ - QuestionBaseGenMixin: Connects questions to language models for response generation
33
+ - AnswerValidatorMixin: Handles validation of answers using response validators
34
+
35
+ 3. Common Workflow:
36
+ - User creates a question instance with specific parameters
37
+ - Question is connected to a language model via the `by()` method
38
+ - The question generates prompts using templates and scenario variables
39
+ - The language model generates a response which is parsed and validated
40
+ - The validated response is returned to the user
41
+
42
+ 4. Extension Points:
43
+ - New question types inherit from QuestionBase and define specialized behavior
44
+ - Custom template files can be defined for specialized prompt generation
45
+ - Response validators can be customized for different validation requirements
46
+ - Integration with the survey system using question_name as a key identifier
47
+ """
48
+
49
+ from __future__ import annotations
50
+ from abc import ABC
51
+ from typing import Any, Type, Optional, List, Callable, Union, TypedDict, TYPE_CHECKING
52
+
53
+ from .exceptions import QuestionSerializationError
54
+
55
+ from ..base import PersistenceMixin, RepresentationMixin, BaseDiff, BaseDiffCollection
56
+ from ..utilities import remove_edsl_version, is_valid_variable_name
57
+
58
+ if TYPE_CHECKING:
59
+ from ..agents import Agent
60
+ from ..scenarios import Scenario
61
+ from ..surveys import Survey
62
+
63
+ from .descriptors import QuestionNameDescriptor, QuestionTextDescriptor
64
+ from .answer_validator_mixin import AnswerValidatorMixin
65
+ from .register_questions_meta import RegisterQuestionsMeta
66
+ from .simple_ask_mixin import SimpleAskMixin
67
+ from .question_base_prompts_mixin import QuestionBasePromptsMixin
68
+ from .question_base_gen_mixin import QuestionBaseGenMixin
69
+
70
+ if TYPE_CHECKING:
71
+ from .response_validator_abc import ResponseValidatorABC
72
+ from ..language_models import LanguageModel
73
+ from ..results import Results
74
+ from ..jobs import Jobs
75
+
76
+
77
+ class QuestionBase(
78
+ PersistenceMixin,
79
+ RepresentationMixin,
80
+ SimpleAskMixin,
81
+ QuestionBasePromptsMixin,
82
+ QuestionBaseGenMixin,
83
+ ABC,
84
+ AnswerValidatorMixin,
85
+ metaclass=RegisterQuestionsMeta,
86
+ ):
87
+ """
88
+ Abstract base class for all question types in EDSL.
89
+
90
+ QuestionBase defines the core interface and behavior that all question types must
91
+ implement. It provides the foundation for asking questions to agents, validating
92
+ responses, generating prompts, and integrating with the rest of the EDSL framework.
93
+
94
+ The class inherits from multiple mixins to provide different capabilities:
95
+ - PersistenceMixin: Serialization and deserialization
96
+ - RepresentationMixin: String representation
97
+ - SimpleAskMixin: Basic asking functionality
98
+ - QuestionBasePromptsMixin: Template-based prompt generation
99
+ - QuestionBaseGenMixin: Generate responses with language models
100
+ - AnswerValidatorMixin: Response validation
101
+
102
+ It also uses the RegisterQuestionsMeta metaclass to enforce constraints on child classes
103
+ and automatically register them for serialization and runtime use.
104
+
105
+ Class attributes:
106
+ question_name (str): Name of the question, used as an identifier
107
+ question_text (str): The actual text of the question to be asked
108
+
109
+ Required attributes in derived classes:
110
+ question_type (str): String identifier for the question type
111
+ _response_model (Type): Pydantic model class for validating responses
112
+ response_validator_class (Type): Validator class for responses
113
+
114
+ Key Methods:
115
+ by(model): Connect this question to a language model for answering
116
+ run(): Execute the question with the connected language model
117
+ duplicate(): Create an exact copy of this question
118
+ is_valid_question_name(): Verify the question_name is valid
119
+
120
+ Lifecycle:
121
+ 1. Instantiation: A question is created with specific parameters
122
+ 2. Connection: The question is connected to a language model via by()
123
+ 3. Execution: The question is run to generate a response
124
+ 4. Validation: The response is validated based on the question type
125
+ 5. Result: The validated response is returned for analysis
126
+
127
+ Template System:
128
+ Questions use Jinja2 templates for generating prompts. Each question type
129
+ has associated template files:
130
+ - answering_instructions.jinja: Instructions for how the model should answer
131
+ - question_presentation.jinja: Format for how the question is presented
132
+ Templates support variable substitution using scenario variables.
133
+
134
+ Response Validation:
135
+ Each question type has a dedicated response validator that:
136
+ - Enforces the expected response structure
137
+ - Ensures the response is valid for the question type
138
+ - Attempts to fix invalid responses when possible
139
+ - Uses Pydantic models for schema validation
140
+
141
+ Example:
142
+ Derived classes must define the required attributes:
143
+
144
+ ```python
145
+ class FreeTextQuestion(QuestionBase):
146
+ question_type = "free_text"
147
+ _response_model = FreeTextResponse
148
+ response_validator_class = FreeTextResponseValidator
149
+
150
+ def __init__(self, question_name, question_text, **kwargs):
151
+ self.question_name = question_name
152
+ self.question_text = question_text
153
+ # Additional initialization as needed
154
+ ```
155
+
156
+ Using a question:
157
+
158
+ ```python
159
+ # Create a question
160
+ question = FreeTextQuestion(
161
+ question_name="opinion",
162
+ question_text="What do you think about AI?"
163
+ )
164
+
165
+ # Connect to a language model and run
166
+ from edsl.language_models import Model
167
+ model = Model()
168
+ result = question.by(model).run()
169
+
170
+ # Access the answer
171
+ answer = result.select("answer.opinion").to_list()[0]
172
+ print(f"The model's opinion: {answer}")
173
+ ```
174
+
175
+ Notes:
176
+ - QuestionBase is abstract and cannot be instantiated directly
177
+ - Child classes must implement required methods and attributes
178
+ - The RegisterQuestionsMeta metaclass handles registration of question types
179
+ - Questions can be serialized to and from dictionaries for storage
180
+ - Questions can be used independently or as part of surveys
181
+ """
182
+
183
+ question_name: str = QuestionNameDescriptor()
184
+ question_text: str = QuestionTextDescriptor()
185
+
186
+ _answering_instructions = None
187
+ _question_presentation = None
188
+
189
+ def is_valid_question_name(self) -> bool:
190
+ """
191
+ Check if the question name is a valid Python identifier.
192
+
193
+ This method validates that the question_name attribute is a valid Python
194
+ variable name according to Python's syntax rules. This is important because
195
+ question names are often used as identifiers in various parts of the system.
196
+
197
+ Returns:
198
+ bool: True if the question name is a valid Python identifier, False otherwise.
199
+
200
+ Examples:
201
+ >>> from edsl.questions import QuestionFreeText
202
+ >>> q = QuestionFreeText(question_name="valid_name", question_text="Text")
203
+ >>> q.is_valid_question_name()
204
+ True
205
+
206
+ >>> q = QuestionFreeText(question_name="123invalid", question_text="Text")
207
+ Traceback (most recent call last):
208
+ ...
209
+ edsl.questions.exceptions.QuestionCreationValidationError: `question_name` is not a valid variable name (got 123invalid).
210
+ """
211
+ return is_valid_variable_name(self.question_name)
212
+
213
+ @property
214
+ def response_validator(self) -> "ResponseValidatorABC":
215
+ """
216
+ Get the appropriate validator for this question type.
217
+
218
+ This property lazily creates and returns a response validator instance specific
219
+ to this question type. The validator is created using the ResponseValidatorFactory,
220
+ which selects the appropriate validator class based on the question's type.
221
+
222
+ Returns:
223
+ ResponseValidatorABC: An instance of the appropriate validator for this question.
224
+
225
+ Notes:
226
+ - Each question type has its own validator class defined in the class attribute
227
+ response_validator_class
228
+ - The validator is responsible for ensuring responses conform to the expected
229
+ format and constraints for this question type
230
+ """
231
+ from .response_validator_factory import ResponseValidatorFactory
232
+
233
+ rvf = ResponseValidatorFactory(self)
234
+ return rvf.response_validator
235
+
236
+ def duplicate(self) -> "QuestionBase":
237
+ """
238
+ Create an exact copy of this question instance.
239
+
240
+ This method creates a new instance of the question with identical attributes
241
+ by serializing the current instance to a dictionary and then deserializing
242
+ it back into a new instance.
243
+
244
+ Returns:
245
+ QuestionBase: A new instance of the same question type with identical attributes.
246
+
247
+ Examples:
248
+ >>> from edsl.questions import QuestionFreeText
249
+ >>> original = QuestionFreeText(question_name="q1", question_text="Hello?")
250
+ >>> copy = original.duplicate()
251
+ >>> original.question_name == copy.question_name
252
+ True
253
+ >>> original is copy
254
+ False
255
+ """
256
+ return self.from_dict(self.to_dict())
257
+
258
+ @property
259
+ def fake_data_factory(self):
260
+ """
261
+ Create and return a factory for generating fake response data.
262
+
263
+ This property lazily creates a factory class based on Pydantic's ModelFactory
264
+ that can generate fake data conforming to the question's response model.
265
+ The factory is cached after first creation for efficiency.
266
+
267
+ Returns:
268
+ ModelFactory: A factory class that can generate fake data for this question type.
269
+
270
+ Notes:
271
+ - Uses polyfactory to generate valid fake data instances
272
+ - The response model for the question defines the structure of the generated data
273
+ - Primarily used for testing and simulation purposes
274
+ """
275
+ if not hasattr(self, "_fake_data_factory"):
276
+ from polyfactory.factories.pydantic_factory import ModelFactory
277
+
278
+ class FakeData(ModelFactory[self.response_model]): ...
279
+
280
+ self._fake_data_factory = FakeData
281
+ return self._fake_data_factory
282
+
283
+ def _simulate_answer(self, human_readable: bool = False) -> dict:
284
+ """
285
+ Generate a simulated valid answer for this question.
286
+
287
+ This method creates a plausible answer that would pass validation for this
288
+ question type. It's primarily used for testing, examples, and debugging purposes.
289
+
290
+ Args:
291
+ human_readable: If True, converts code-based answers to their human-readable
292
+ text equivalents for multiple choice and similar questions.
293
+
294
+ Returns:
295
+ dict: A dictionary containing a simulated valid answer with appropriate
296
+ structure for this question type.
297
+
298
+ Examples:
299
+ >>> from edsl import QuestionFreeText as Q
300
+ >>> answer = Q.example()._simulate_answer()
301
+ >>> "answer" in answer and "generated_tokens" in answer
302
+ True
303
+
304
+ Notes:
305
+ - Free text questions have special handling with a predefined response
306
+ - Other question types use the fake_data_factory to generate valid responses
307
+ - For questions with options, the human_readable parameter determines whether
308
+ indices or actual text options are returned
309
+ """
310
+ if self.question_type == "free_text":
311
+ return {"answer": "Hello, how are you?", 'generated_tokens': "Hello, how are you?"}
312
+
313
+ simulated_answer = self.fake_data_factory.build().dict()
314
+ if human_readable and hasattr(self, "question_options") and self.use_code:
315
+ simulated_answer["answer"] = [
316
+ self.question_options[index] for index in simulated_answer["answer"]
317
+ ]
318
+ return simulated_answer
319
+
320
+ class ValidatedAnswer(TypedDict):
321
+ """
322
+ Type definition for a validated answer to a question.
323
+
324
+ This TypedDict defines the structure of a validated answer, which includes
325
+ the actual answer value, an optional comment, and optional generated tokens
326
+ information for tracking LLM token usage.
327
+
328
+ Attributes:
329
+ answer: The validated answer value, type depends on question type
330
+ comment: Optional string comment or explanation for the answer
331
+ generated_tokens: Optional string containing raw LLM output for token tracking
332
+ """
333
+ answer: Any
334
+ comment: Optional[str]
335
+ generated_tokens: Optional[str]
336
+
337
+ def _validate_answer(
338
+ self, answer: dict, replacement_dict: dict = None
339
+ ) -> ValidatedAnswer:
340
+ """
341
+ Validate a raw answer against this question's constraints.
342
+
343
+ This method applies the appropriate validator for this question type to the
344
+ provided answer dictionary, ensuring it conforms to the expected structure
345
+ and constraints.
346
+
347
+ Args:
348
+ answer: Dictionary containing the raw answer to validate.
349
+ replacement_dict: Optional dictionary of replacements to apply during
350
+ validation for template variables.
351
+
352
+ Returns:
353
+ ValidatedAnswer: A dictionary containing the validated answer with the
354
+ structure defined by ValidatedAnswer TypedDict.
355
+
356
+ Raises:
357
+ QuestionAnswerValidationError: If the answer fails validation.
358
+
359
+ Examples:
360
+ >>> from edsl.questions import QuestionFreeText as Q
361
+ >>> Q.example()._validate_answer({'answer': 'Hello', 'generated_tokens': 'Hello'})
362
+ {'answer': 'Hello', 'generated_tokens': 'Hello'}
363
+ """
364
+ return self.response_validator.validate(answer, replacement_dict)
365
+
366
+ @property
367
+ def name(self) -> str:
368
+ """
369
+ Get the question name.
370
+
371
+ This property is a simple alias for question_name that provides a consistent
372
+ interface shared with other EDSL components like Instructions.
373
+
374
+ Returns:
375
+ str: The question name.
376
+ """
377
+ return self.question_name
378
+
379
+ def __hash__(self) -> int:
380
+ """
381
+ Calculate a hash value for this question instance.
382
+
383
+ This method returns a deterministic hash based on the serialized dictionary
384
+ representation of the question. This allows questions to be used in sets and
385
+ as dictionary keys.
386
+
387
+ Returns:
388
+ int: A hash value for this question.
389
+
390
+ Examples:
391
+ >>> from edsl import QuestionFreeText as Q
392
+ >>> q1 = Q.example()
393
+ >>> q2 = q1.duplicate()
394
+ >>> hash(q1) == hash(q2)
395
+ True
396
+ """
397
+ from ..utilities import dict_hash
398
+
399
+ return dict_hash(self.to_dict(add_edsl_version=False))
400
+
401
+ @property
402
+ def data(self) -> dict:
403
+ """Return a dictionary of question attributes **except** for question_type.
404
+
405
+ >>> from edsl.questions import QuestionFreeText as Q
406
+ >>> Q.example().data
407
+ {'question_name': 'how_are_you', 'question_text': 'How are you?'}
408
+ """
409
+ exclude_list = [
410
+ "question_type",
411
+ # "_include_comment",
412
+ "_fake_data_factory",
413
+ # "_use_code",
414
+ "_model_instructions",
415
+ ]
416
+ only_if_not_na_list = ["_answering_instructions", "_question_presentation"]
417
+
418
+ only_if_not_default_list = {"_include_comment": True, "_use_code": False}
419
+
420
+ def ok(key, value):
421
+ if not key.startswith("_"):
422
+ return False
423
+ if key in exclude_list:
424
+ return False
425
+ if key in only_if_not_na_list and value is None:
426
+ return False
427
+ if (
428
+ key in only_if_not_default_list
429
+ and value == only_if_not_default_list[key]
430
+ ):
431
+ return False
432
+
433
+ return True
434
+
435
+ candidate_data = {
436
+ k.replace("_", "", 1): v for k, v in self.__dict__.items() if ok(k, v)
437
+ }
438
+
439
+ if "func" in candidate_data:
440
+ func = candidate_data.pop("func")
441
+ import inspect
442
+
443
+ candidate_data["function_source_code"] = inspect.getsource(func)
444
+
445
+ return candidate_data
446
+
447
+ def to_dict(self, add_edsl_version: bool = True):
448
+ """Convert the question to a dictionary that includes the question type (used in deserialization).
449
+
450
+ >>> from edsl.questions import QuestionFreeText as Q; Q.example().to_dict(add_edsl_version = False)
451
+ {'question_name': 'how_are_you', 'question_text': 'How are you?', 'question_type': 'free_text'}
452
+ """
453
+ candidate_data = self.data.copy()
454
+ candidate_data["question_type"] = self.question_type
455
+ d = {key: value for key, value in candidate_data.items() if value is not None}
456
+ if add_edsl_version:
457
+ from .. import __version__
458
+
459
+ d["edsl_version"] = __version__
460
+ d["edsl_class_name"] = "QuestionBase"
461
+
462
+ return d
463
+
464
+ @classmethod
465
+ @remove_edsl_version
466
+ def from_dict(cls, data: dict) -> "QuestionBase":
467
+ """
468
+ Create a question instance from a dictionary representation.
469
+
470
+ This class method deserializes a question from a dictionary representation,
471
+ typically created by the to_dict method. It looks up the appropriate question
472
+ class based on the question_type field and constructs an instance of that class.
473
+
474
+ Args:
475
+ data: Dictionary representation of a question, must contain a 'question_type' field.
476
+
477
+ Returns:
478
+ QuestionBase: An instance of the appropriate question subclass.
479
+
480
+ Raises:
481
+ QuestionSerializationError: If the data is missing the question_type field or
482
+ if no question class is registered for the given type.
483
+
484
+ Examples:
485
+ >>> from edsl.questions import QuestionFreeText
486
+ >>> original = QuestionFreeText.example()
487
+ >>> serialized = original.to_dict()
488
+ >>> deserialized = QuestionBase.from_dict(serialized)
489
+ >>> original.question_text == deserialized.question_text
490
+ True
491
+ >>> isinstance(deserialized, QuestionFreeText)
492
+ True
493
+
494
+ Notes:
495
+ - The @remove_edsl_version decorator removes EDSL version information from the
496
+ dictionary before processing
497
+ - Special handling is implemented for certain question types like linear_scale
498
+ - Model instructions, if present, are handled separately to ensure proper initialization
499
+ """
500
+ local_data = data.copy()
501
+
502
+ try:
503
+ question_type = local_data.pop("question_type")
504
+ if question_type == "linear_scale":
505
+ # This is a fix for issue https://github.com/expectedparrot/edsl/issues/165
506
+ options_labels = local_data.get("option_labels", None)
507
+ if options_labels:
508
+ options_labels = {
509
+ int(key): value for key, value in options_labels.items()
510
+ }
511
+ local_data["option_labels"] = options_labels
512
+ except:
513
+ raise QuestionSerializationError(
514
+ f"Data does not have a 'question_type' field (got {data})."
515
+ )
516
+ from .question_registry import get_question_class
517
+
518
+ try:
519
+ question_class = get_question_class(question_type)
520
+ except ValueError:
521
+ raise QuestionSerializationError(
522
+ f"No question registered with question_type {question_type}"
523
+ )
524
+
525
+ if "model_instructions" in local_data:
526
+ model_instructions = local_data.pop("model_instructions")
527
+ new_q = question_class(**local_data)
528
+ new_q.model_instructions = model_instructions
529
+ return new_q
530
+
531
+ return question_class(**local_data)
532
+
533
+ @classmethod
534
+ def _get_test_model(cls, canned_response: Optional[str] = None) -> "LanguageModel":
535
+ """
536
+ Create a test language model with optional predefined response.
537
+
538
+ This helper method creates a test language model that can be used for testing
539
+ questions without making actual API calls to language model providers.
540
+
541
+ Args:
542
+ canned_response: Optional predefined response the model will return for any prompt.
543
+
544
+ Returns:
545
+ LanguageModel: A test language model instance.
546
+
547
+ Notes:
548
+ - The test model does not make external API calls
549
+ - When canned_response is provided, the model will always return that response
550
+ - Used primarily for testing, demonstrations, and examples
551
+ """
552
+ from ..language_models import LanguageModel
553
+
554
+ return LanguageModel.example(canned_response=canned_response, test_model=True)
555
+
556
+ @classmethod
557
+ def run_example(
558
+ cls,
559
+ show_answer: bool = True,
560
+ model: Optional["LanguageModel"] = None,
561
+ cache: bool = False,
562
+ disable_remote_cache: bool = False,
563
+ disable_remote_inference: bool = False,
564
+ **kwargs,
565
+ ) -> "Results":
566
+ """
567
+ Run the example question with a language model and return results.
568
+
569
+ This class method creates an example instance of the question, asks it using
570
+ the provided language model, and returns the results. It's primarily used for
571
+ demonstrations, documentation, and testing.
572
+
573
+ Args:
574
+ show_answer: If True, returns only the answer portion of the results.
575
+ If False, returns the full results.
576
+ model: Language model to use for answering. If None, creates a default model.
577
+ cache: Whether to use local caching for the model call.
578
+ disable_remote_cache: Whether to disable remote caching.
579
+ disable_remote_inference: Whether to disable remote inference.
580
+ **kwargs: Additional keyword arguments to pass to the example method.
581
+
582
+ Returns:
583
+ Results: Either the full results or just the answer portion, depending on show_answer.
584
+
585
+ Examples:
586
+ >>> from edsl.language_models import LanguageModel
587
+ >>> from edsl import QuestionFreeText as Q
588
+ >>> m = Q._get_test_model(canned_response="Yo, what's up?")
589
+ >>> results = Q.run_example(show_answer=True, model=m,
590
+ ... disable_remote_cache=True, disable_remote_inference=True)
591
+ >>> "answer" in str(results)
592
+ True
593
+
594
+ Notes:
595
+ - This method is useful for quick demonstrations of question behavior
596
+ - The disable_remote_* parameters are useful for offline testing
597
+ - Additional parameters to customize the example can be passed via kwargs
598
+ """
599
+ if model is None:
600
+ from ..language_models import Model
601
+
602
+ model = Model()
603
+ results = (
604
+ cls.example(**kwargs)
605
+ .by(model)
606
+ .run(
607
+ cache=cache,
608
+ disable_remote_cache=disable_remote_cache,
609
+ disable_remote_inference=disable_remote_inference,
610
+ )
611
+ )
612
+ if show_answer:
613
+ return results.select("answer.*")
614
+ else:
615
+ return results
616
+
617
+ def __call__(
618
+ self,
619
+ just_answer: bool = True,
620
+ model: Optional["LanguageModel"] = None,
621
+ agent: Optional["Agent"] = None,
622
+ disable_remote_cache: bool = False,
623
+ disable_remote_inference: bool = False,
624
+ verbose: bool = False,
625
+ **kwargs,
626
+ ) -> Union[Any, "Results"]:
627
+ """Call the question.
628
+
629
+
630
+ >>> from edsl import QuestionFreeText as Q
631
+ >>> from edsl import Model
632
+ >>> m = Model("test", canned_response = "Yo, what's up?")
633
+ >>> q = Q(question_name = "color", question_text = "What is your favorite color?")
634
+ >>> q(model = m, disable_remote_cache = True, disable_remote_inference = True, cache = False)
635
+ "Yo, what's up?"
636
+
637
+ """
638
+ survey = self.to_survey()
639
+ results = survey(
640
+ model=model,
641
+ agent=agent,
642
+ **kwargs,
643
+ verbose=verbose,
644
+ disable_remote_cache=disable_remote_cache,
645
+ disable_remote_inference=disable_remote_inference,
646
+ )
647
+ if just_answer:
648
+ return results.select(f"answer.{self.question_name}").first()
649
+ else:
650
+ return results
651
+
652
+ def run(self, *args, **kwargs) -> "Results":
653
+ """Turn a single question into a survey and runs it."""
654
+ return self.to_survey().run(*args, **kwargs)
655
+
656
+ def using(self, *args, **kwargs) -> "Jobs":
657
+ """Turn a single question into a survey and then a Job."""
658
+ return self.to_survey().to_jobs().using(*args, **kwargs)
659
+
660
+ async def run_async(
661
+ self,
662
+ just_answer: bool = True,
663
+ model: Optional["LanguageModel"] = None,
664
+ agent: Optional["Agent"] = None,
665
+ disable_remote_inference: bool = False,
666
+ **kwargs,
667
+ ) -> Union[Any, "Results"]:
668
+ """Call the question asynchronously.
669
+
670
+ >>> import asyncio
671
+ >>> from edsl.questions import QuestionFreeText as Q
672
+ >>> m = Q._get_test_model(canned_response = "Blue")
673
+ >>> q = Q(question_name = "color", question_text = "What is your favorite color?")
674
+ >>> async def test_run_async(): result = await q.run_async(model=m, disable_remote_inference = True, disable_remote_cache = True); print(result)
675
+ >>> asyncio.run(test_run_async())
676
+ Blue
677
+ """
678
+ survey = self.to_survey()
679
+ results = await survey.run_async(
680
+ model=model,
681
+ agent=agent,
682
+ disable_remote_inference=disable_remote_inference,
683
+ **kwargs,
684
+ )
685
+ if just_answer:
686
+ return results.select(f"answer.{self.question_name}").first()
687
+ else:
688
+ return results
689
+
690
+ def __getitem__(self, key: str) -> Any:
691
+ """Get an attribute of the question so it can be treated like a dictionary.
692
+
693
+ >>> from edsl.questions import QuestionFreeText as Q
694
+ >>> Q.example()['question_text']
695
+ 'How are you?'
696
+ """
697
+ try:
698
+ return getattr(self, key)
699
+ except TypeError:
700
+ raise KeyError(f"Question has no attribute {key} of type {type(key)}")
701
+
702
+ def __repr__(self) -> str:
703
+ """Return a string representation of the question. Should be able to be used to reconstruct the question.
704
+
705
+ >>> from edsl import QuestionFreeText as Q
706
+ >>> repr(Q.example())
707
+ 'Question(\\'free_text\\', question_name = \"""how_are_you\""", question_text = \"""How are you?\""")'
708
+ """
709
+ items = [
710
+ f'{k} = """{v}"""' if isinstance(v, str) else f"{k} = {v}"
711
+ for k, v in self.data.items()
712
+ if k != "question_type"
713
+ ]
714
+ question_type = self.to_dict().get("question_type", "None")
715
+ return f"Question('{question_type}', {', '.join(items)})"
716
+
717
+ def __eq__(self, other: Union[Any, Type[QuestionBase]]) -> bool:
718
+ """Check if two questions are equal. Equality is defined as having the .to_dict().
719
+
720
+ >>> from edsl import QuestionFreeText as Q
721
+ >>> q1 = Q.example()
722
+ >>> q2 = Q.example()
723
+ >>> q1 == q2
724
+ True
725
+ >>> q1.question_text = "How are you John?"
726
+ >>> q1 == q2
727
+ False
728
+
729
+ """
730
+ return hash(self) == hash(other)
731
+
732
+ def __sub__(self, other) -> BaseDiff:
733
+ """Return the difference between two objects.
734
+ >>> from edsl import QuestionFreeText as Q
735
+ >>> q1 = Q.example()
736
+ >>> q2 = q1.copy()
737
+ >>> q2.question_text = "How are you John?"
738
+ >>> diff = q1 - q2
739
+ """
740
+
741
+ return BaseDiff(other, self)
742
+
743
+ # TODO: Throws an error that should be addressed at QuestionFunctional
744
+ def __add__(self, other_question_or_diff):
745
+ """
746
+ Compose two questions into a single question.
747
+ """
748
+ if isinstance(other_question_or_diff, BaseDiff) or isinstance(
749
+ other_question_or_diff, BaseDiffCollection
750
+ ):
751
+ return other_question_or_diff.apply(self)
752
+
753
+ def _translate_answer_code_to_answer(
754
+ self, answer, scenario: Optional["Scenario"] = None
755
+ ):
756
+ """There is over-ridden by child classes that ask for codes."""
757
+ return answer
758
+
759
+ def add_question(self, other: QuestionBase) -> "Survey":
760
+ """Add a question to this question by turning them into a survey with two questions.
761
+
762
+ >>> from edsl.questions import QuestionFreeText as Q
763
+ >>> from edsl.questions import QuestionMultipleChoice as QMC
764
+ >>> s = Q.example().add_question(QMC.example())
765
+ >>> len(s.questions)
766
+ 2
767
+ """
768
+ return self.to_survey().add_question(other)
769
+
770
+ def to_survey(self) -> "Survey":
771
+ """Turn a single question into a survey.
772
+ >>> from edsl import QuestionFreeText as Q
773
+ >>> Q.example().to_survey().questions[0].question_name
774
+ 'how_are_you'
775
+ """
776
+ from ..surveys import Survey
777
+
778
+ return Survey([self])
779
+
780
+ def humanize(
781
+ self,
782
+ project_name: str = "Project",
783
+ survey_description: Optional[str] = None,
784
+ survey_alias: Optional[str] = None,
785
+ survey_visibility: Optional["VisibilityType"] = "unlisted",
786
+ ) -> dict:
787
+ """
788
+ Turn a single question into a survey and send the survey to Coop.
789
+
790
+ Then, create a project on Coop so you can share the survey with human respondents.
791
+ """
792
+ s = self.to_survey()
793
+ project_details = s.humanize(
794
+ project_name, survey_description, survey_alias, survey_visibility
795
+ )
796
+ return project_details
797
+
798
+ def by(self, *args) -> "Jobs":
799
+ """Turn a single question into a survey and then a Job."""
800
+ from ..surveys import Survey
801
+
802
+ s = Survey([self])
803
+ return s.by(*args)
804
+
805
+ def human_readable(self) -> str:
806
+ """Print the question in a human readable format.
807
+
808
+ >>> from edsl.questions import QuestionFreeText
809
+ >>> QuestionFreeText.example().human_readable()
810
+ 'Question Type: free_text\\nQuestion: How are you?'
811
+ """
812
+ lines = []
813
+ lines.append(f"Question Type: {self.question_type}")
814
+ lines.append(f"Question: {self.question_text}")
815
+ if hasattr(self, "question_options"):
816
+ lines.append("Please name the option you choose from the following.:")
817
+ for index, option in enumerate(self.question_options):
818
+ lines.append(f"{option}")
819
+ return "\n".join(lines)
820
+
821
+ def html(
822
+ self,
823
+ scenario: Optional[dict] = None,
824
+ agent: Optional[dict] = {},
825
+ answers: Optional[dict] = None,
826
+ include_question_name: bool = False,
827
+ height: Optional[int] = None,
828
+ width: Optional[int] = None,
829
+ iframe=False,
830
+ ):
831
+ from ..questions.HTMLQuestion import HTMLQuestion
832
+
833
+ return HTMLQuestion(self).html(
834
+ scenario, agent, answers, include_question_name, height, width, iframe
835
+ )
836
+
837
+ @classmethod
838
+ def example_model(cls):
839
+ from ..language_models import Model
840
+
841
+ q = cls.example()
842
+ m = Model("test", canned_response=cls._simulate_answer(q)["answer"])
843
+
844
+ return m
845
+
846
+ @classmethod
847
+ def example_results(cls):
848
+ m = cls.example_model()
849
+ q = cls.example()
850
+ return q.by(m).run(cache=False)
851
+
852
+ def rich_print(self):
853
+ """Print the question in a rich format."""
854
+ from rich.table import Table
855
+
856
+ table = Table(show_header=True, header_style="bold magenta")
857
+ table.add_column("Question Name", style="dim")
858
+ table.add_column("Question Type")
859
+ table.add_column("Question Text")
860
+ table.add_column("Options")
861
+
862
+ question = self
863
+ if hasattr(question, "question_options"):
864
+ options = ", ".join([str(o) for o in question.question_options])
865
+ else:
866
+ options = "None"
867
+ table.add_row(
868
+ question.question_name,
869
+ question.question_type,
870
+ question.question_text,
871
+ options,
872
+ )
873
+ return table
874
+
875
+ # endregion
876
+
877
+
878
+ if __name__ == "__main__":
879
+ import doctest
880
+
881
+ doctest.testmod(optionflags=doctest.ELLIPSIS)