edsl 0.1.47__py3-none-any.whl → 0.1.48__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 +303 -67
  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.48.dist-info}/METADATA +1 -1
  230. edsl-0.1.48.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.48.dist-info}/LICENSE +0 -0
  314. {edsl-0.1.47.dist-info → edsl-0.1.48.dist-info}/WHEEL +0 -0
@@ -1,28 +1,157 @@
1
- # Schemas
2
- from edsl.questions.settings import Settings
3
- from edsl.questions.register_questions_meta import RegisterQuestionsMeta
1
+ """
2
+ EDSL Questions Module: The core system for creating and processing questions.
4
3
 
5
- # Base Class
6
- from edsl.questions.QuestionBase import QuestionBase
4
+ The questions module provides a comprehensive framework for creating, validating,
5
+ and processing various types of questions that can be asked to language models.
6
+ It is one of the foundational components of EDSL and enables the creation of
7
+ surveys, interviews, and other question-based interactions.
8
+
9
+ Key Features:
10
+ -------------
11
+ - A wide variety of question types including free text, multiple choice, checkbox, etc.
12
+ - Consistent interface for asking questions to language models
13
+ - Robust validation of responses
14
+ - Support for question templates and parameterization with scenarios
15
+ - Integration with the rest of the EDSL framework
16
+ - Extensible architecture for creating custom question types
17
+
18
+ Question Types:
19
+ --------------
20
+ Core Question Types:
21
+ - QuestionFreeText: Free-form text responses without constraints
22
+ - QuestionMultipleChoice: Selection from a predefined list of options
23
+ - QuestionCheckBox: Selection of multiple options from a predefined list
24
+ - QuestionNumerical: Numeric responses within an optional range
25
+ - QuestionList: Responses in the form of lists or arrays
26
+ - QuestionDict: Responses with key-value pairs
27
+ - QuestionMatrix: Grid-based responses with rows and columns
28
+ - QuestionBudget: Allocation of a budget across multiple options
29
+ - QuestionRank: Ordering of items by preference or other criteria
30
+ - QuestionExtract: Extraction of specific information from text or data
31
+
32
+ Derived Question Types:
33
+ - QuestionLikertFive: Standard 5-point Likert scale (agree/disagree)
34
+ - QuestionLinearScale: Linear scale with customizable range and labels
35
+ - QuestionYesNo: Simple binary yes/no response
36
+ - QuestionTopK: Selection of top K items from a list of options
37
+
38
+ Technical Architecture:
39
+ ---------------------
40
+ 1. Base Classes and Mixins:
41
+ - QuestionBase: Abstract base class for all question types
42
+ - SimpleAskMixin: Basic asking functionality to models and agents
43
+ - AnswerValidatorMixin: Validation of responses
44
+ - QuestionBasePromptsMixin: Template-based prompt generation
45
+ - QuestionBaseGenMixin: Integration with language models
46
+
47
+ 2. Validation System:
48
+ - Response validators ensure answers conform to expected formats
49
+ - Pydantic models provide schema validation
50
+ - Repair functionality attempts to fix invalid responses
51
+
52
+ 3. Template System:
53
+ - Jinja2 templates for consistent prompt generation
54
+ - Separate templates for answering instructions and question presentation
55
+ - Support for dynamic content through scenario variables
56
+
57
+ 4. Registry System:
58
+ - RegisterQuestionsMeta metaclass for automatic registration
59
+ - Question types are automatically available for serialization
60
+ - Registry enables runtime lookup of question types
61
+
62
+ Example Usage:
63
+ -------------
64
+ >>> from edsl import QuestionFreeText
65
+ >>> question = QuestionFreeText(
66
+ ... question_name="greeting",
67
+ ... question_text="Say hello to the user."
68
+ ... )
69
+ >>> from edsl.language_models import Model
70
+ >>> model = Model()
71
+ >>> # result = question.by(model).run()
72
+
73
+ >>> from edsl import QuestionMultipleChoice
74
+ >>> choice_q = QuestionMultipleChoice(
75
+ ... question_name="preference",
76
+ ... question_text="Which color do you prefer?",
77
+ ... question_options=["Red", "Blue", "Green", "Yellow"]
78
+ ... )
79
+ >>> # result = choice_q.by(model).run()
80
+
81
+ Integration with Surveys:
82
+ -----------------------
83
+ Questions can be combined into surveys for more complex interactions:
84
+
85
+ # Note: Actual survey usage in code
86
+ # from edsl import Survey
87
+ # survey = Survey()
88
+ # survey.add_question(question)
89
+ # survey.add_question(choice_q)
90
+ # results = survey.by(model).run()
91
+
92
+ Extension Points:
93
+ ---------------
94
+ The questions module is designed to be extensible:
95
+ - Create custom question types by subclassing QuestionBase
96
+ - Implement custom validators for specialized validation
97
+ - Define custom templates for unique presentation needs
98
+ - Combine questions in surveys with custom flow logic
99
+ """
100
+
101
+ # Schemas and metadata
102
+ from .settings import Settings
103
+ from .register_questions_meta import RegisterQuestionsMeta
104
+
105
+ # Base Class and registry
106
+ from .question_base import QuestionBase
107
+ from .question_registry import Question
7
108
 
8
109
  # Core Questions
9
- from edsl.questions.QuestionCheckBox import QuestionCheckBox
10
- from edsl.questions.QuestionExtract import QuestionExtract
11
- from edsl.questions.QuestionFreeText import QuestionFreeText
12
- from edsl.questions.QuestionFunctional import QuestionFunctional
13
- from edsl.questions.QuestionList import QuestionList
14
- from edsl.questions.QuestionMatrix import QuestionMatrix
15
- from edsl.questions.QuestionDict import QuestionDict
16
- from edsl.questions.QuestionMultipleChoice import QuestionMultipleChoice
17
- from edsl.questions.QuestionNumerical import QuestionNumerical
18
- from edsl.questions.QuestionBudget import QuestionBudget
19
- from edsl.questions.QuestionRank import QuestionRank
20
-
21
- # # # Questions derived from core questions
22
- from edsl.questions.derived.QuestionLikertFive import QuestionLikertFive
23
- from edsl.questions.derived.QuestionLinearScale import QuestionLinearScale
24
- from edsl.questions.derived.QuestionYesNo import QuestionYesNo
25
- from edsl.questions.derived.QuestionTopK import QuestionTopK
26
-
27
- # # Compose Questions
28
- # from edsl.questions.compose_questions import compose_questions
110
+ from .question_check_box import QuestionCheckBox
111
+ from .question_extract import QuestionExtract
112
+ from .question_free_text import QuestionFreeText
113
+ from .question_functional import QuestionFunctional
114
+ from .question_list import QuestionList
115
+ from .question_matrix import QuestionMatrix
116
+ from .question_dict import QuestionDict
117
+ from .question_multiple_choice import QuestionMultipleChoice
118
+ from .question_numerical import QuestionNumerical
119
+ from .question_budget import QuestionBudget
120
+ from .question_rank import QuestionRank
121
+
122
+ # Questions derived from core questions
123
+ from .derived.question_likert_five import QuestionLikertFive
124
+ from .derived.question_linear_scale import QuestionLinearScale
125
+ from .derived.question_yes_no import QuestionYesNo
126
+ from .derived.question_top_k import QuestionTopK
127
+
128
+ from .exceptions import QuestionScenarioRenderError
129
+
130
+ __all__ = [
131
+ # Schema and metadata
132
+ "Settings",
133
+ "RegisterQuestionsMeta",
134
+
135
+ # Base question class and registry
136
+ "QuestionBase",
137
+ "Question",
138
+
139
+ # Core question types
140
+ "QuestionFreeText",
141
+ "QuestionMultipleChoice",
142
+ "QuestionCheckBox",
143
+ "QuestionDict",
144
+ "QuestionExtract",
145
+ "QuestionFunctional",
146
+ "QuestionList",
147
+ "QuestionMatrix",
148
+ "QuestionNumerical",
149
+ "QuestionBudget",
150
+ "QuestionRank",
151
+
152
+ # Derived question types
153
+ "QuestionLinearScale",
154
+ "QuestionTopK",
155
+ "QuestionLikertFive",
156
+ "QuestionYesNo",
157
+ ]
@@ -2,7 +2,7 @@
2
2
 
3
3
  import re
4
4
  from typing import Any, Type, Union
5
- from edsl.exceptions.questions import (
5
+ from .exceptions import (
6
6
  QuestionAnswerValidationError,
7
7
  )
8
8
 
@@ -1,8 +1,9 @@
1
1
  """Compose two questions where the answer to q1 is used as an input to q2."""
2
2
 
3
- from edsl.questions import QuestionFunctional
4
- from edsl.questions.QuestionBase import QuestionBase
5
- from edsl.scenarios.Scenario import Scenario
3
+ from .question_functional import QuestionFunctional
4
+ from .question_base import QuestionBase
5
+ from ..scenarios import Scenario
6
+ from ..agents import Agent
6
7
 
7
8
 
8
9
  def compose_questions(
@@ -0,0 +1,166 @@
1
+ from __future__ import annotations
2
+ from typing import Optional
3
+ from ..question_multiple_choice import QuestionMultipleChoice
4
+ from ..decorators import inject_exception
5
+
6
+
7
+ class QuestionLikertFive(QuestionMultipleChoice):
8
+ """
9
+ A specialized question that prompts the agent to respond on a 5-point Likert scale.
10
+
11
+ QuestionLikertFive is a subclass of QuestionMultipleChoice that presents a standard
12
+ 5-point Likert scale ranging from "Strongly disagree" to "Strongly agree". This
13
+ question type is ideal for measuring attitudes, opinions, and perceptions on a
14
+ symmetric agree-disagree scale.
15
+
16
+ Key Features:
17
+ - Pre-configured with standard Likert scale options
18
+ - Simplifies creating consistent Likert scale questions
19
+ - Inherits all functionality from QuestionMultipleChoice
20
+ - Can be customized with different options if needed
21
+
22
+ Technical Details:
23
+ - Uses the standard Likert options: ["Strongly disagree", "Disagree", "Neutral", "Agree", "Strongly agree"]
24
+ - Implements `use_code=False` by default, so responses contain the text labels
25
+ - Can be connected to language models and agents like any other question
26
+
27
+ Examples:
28
+ Basic usage:
29
+
30
+ ```python
31
+ q = QuestionLikertFive(
32
+ question_name="climate_concern",
33
+ question_text="I am concerned about climate change."
34
+ )
35
+ ```
36
+
37
+ With custom options:
38
+
39
+ ```python
40
+ q = QuestionLikertFive(
41
+ question_name="satisfaction",
42
+ question_text="I am satisfied with the product.",
43
+ question_options=["Strongly Disagree", "Somewhat Disagree",
44
+ "Neither Agree nor Disagree",
45
+ "Somewhat Agree", "Strongly Agree"]
46
+ )
47
+ ```
48
+
49
+ Notes:
50
+ - Likert scales are particularly useful for surveys and opinion research
51
+ - The default options follow the most common 5-point Likert scale format
52
+ - For different scales (e.g., 7-point), you can pass custom options
53
+ """
54
+
55
+ question_type = "likert_five"
56
+ likert_options: list[str] = [
57
+ "Strongly disagree",
58
+ "Disagree",
59
+ "Neutral",
60
+ "Agree",
61
+ "Strongly agree",
62
+ ]
63
+
64
+ def __init__(
65
+ self,
66
+ question_name: str,
67
+ question_text: str,
68
+ question_options: Optional[list[str]] = None,
69
+ answering_instructions: Optional[str] = None,
70
+ question_presentation: Optional[str] = None,
71
+ include_comment: bool = True,
72
+ ):
73
+ """
74
+ Initialize a new 5-point Likert scale question.
75
+
76
+ Parameters
77
+ ----------
78
+ question_name : str
79
+ The name of the question, used as an identifier. Must be a valid Python variable name.
80
+ This name will be used in results, templates, and when referencing the question in surveys.
81
+
82
+ question_text : str
83
+ The statement to which the agent will respond on the Likert scale. This is typically
84
+ phrased as a statement rather than a question, e.g., "I enjoy using this product."
85
+
86
+ question_options : Optional[list[str]], default=None
87
+ Optional custom Likert scale options. If None, the default 5-point Likert scale options
88
+ are used: ["Strongly disagree", "Disagree", "Neutral", "Agree", "Strongly agree"].
89
+ Custom options should follow the same format (typically 5 or 7 points from negative to positive).
90
+
91
+ answering_instructions : Optional[str], default=None
92
+ Custom instructions for how the model should answer the question. If None,
93
+ default instructions for Likert scale questions will be used.
94
+
95
+ question_presentation : Optional[str], default=None
96
+ Custom template for how the question is presented to the model. If None,
97
+ the default presentation for Likert scale questions will be used.
98
+
99
+ include_comment : bool, default=True
100
+ Whether to include a comment field in the response, allowing the model to provide
101
+ additional explanation beyond just selecting an option on the scale.
102
+
103
+ Examples
104
+ --------
105
+ >>> q = QuestionLikertFive(
106
+ ... question_name="product_satisfaction",
107
+ ... question_text="I am satisfied with the product."
108
+ ... )
109
+
110
+ >>> q_custom = QuestionLikertFive(
111
+ ... question_name="service_quality",
112
+ ... question_text="The service quality was excellent.",
113
+ ... question_options=["Completely Disagree", "Somewhat Disagree",
114
+ ... "Neutral", "Somewhat Agree", "Completely Agree"]
115
+ ... )
116
+
117
+ Notes
118
+ -----
119
+ - The default Likert options can be accessed via QuestionLikertFive.likert_options
120
+ - Likert scale questions inherently use text labels rather than numeric codes
121
+ - The statement should be phrased such that agreeing or disagreeing makes sense
122
+ """
123
+ # Use default Likert options if none are provided
124
+ if question_options is None:
125
+ question_options = self.likert_options
126
+ super().__init__(
127
+ question_name=question_name,
128
+ question_text=question_text,
129
+ question_options=question_options,
130
+ use_code=False,
131
+ include_comment=include_comment,
132
+ answering_instructions=answering_instructions,
133
+ question_presentation=question_presentation,
134
+ )
135
+
136
+ @classmethod
137
+ @inject_exception
138
+ def example(cls) -> QuestionLikertFive:
139
+ """Return an example question."""
140
+ return cls(
141
+ question_name="happy_raining",
142
+ question_text="I'm only happy when it rains.",
143
+ )
144
+
145
+
146
+ def main():
147
+ """Test QuestionLikertFive."""
148
+ # Use the class directly since we're already in the module
149
+ q = QuestionLikertFive.example()
150
+ q.question_text
151
+ q.question_options
152
+ q.question_name
153
+ # validate an answer
154
+ q._validate_answer({"answer": 0, "comment": "I like custard"})
155
+ # translate answer code
156
+ q._translate_answer_code_to_answer(0, {})
157
+ q._simulate_answer()
158
+ q._simulate_answer(human_readable=False)
159
+ q._validate_answer(q._simulate_answer(human_readable=False))
160
+ # serialization (inherits from Question)
161
+ q.to_dict()
162
+ assert q.from_dict(q.to_dict()) == q
163
+
164
+ import doctest
165
+
166
+ doctest.testmod(optionflags=doctest.ELLIPSIS)
@@ -1,10 +1,10 @@
1
1
  from __future__ import annotations
2
2
  from typing import Optional
3
3
 
4
- from edsl.questions.descriptors import QuestionOptionsDescriptor, OptionLabelDescriptor
5
- from edsl.questions.QuestionMultipleChoice import QuestionMultipleChoice
4
+ from ..descriptors import QuestionOptionsDescriptor, OptionLabelDescriptor
5
+ from ..question_multiple_choice import QuestionMultipleChoice
6
6
 
7
- from edsl.questions.decorators import inject_exception
7
+ from ..decorators import inject_exception
8
8
 
9
9
 
10
10
  class QuestionLinearScale(QuestionMultipleChoice):
@@ -67,7 +67,7 @@ class QuestionLinearScale(QuestionMultipleChoice):
67
67
 
68
68
  def main():
69
69
  """Create an example of a linear scale question and demonstrate its functionality."""
70
- from edsl.questions.derived.QuestionLinearScale import QuestionLinearScale
70
+ from edsl.questions import QuestionLinearScale
71
71
 
72
72
  q = QuestionLinearScale.example()
73
73
  q.question_text
@@ -1,9 +1,9 @@
1
1
  from __future__ import annotations
2
2
  from typing import Optional
3
3
 
4
- from edsl.exceptions.questions import QuestionCreationValidationError
5
- from edsl.questions.QuestionCheckBox import QuestionCheckBox
6
- from edsl.questions.decorators import inject_exception
4
+ from ..exceptions import QuestionCreationValidationError
5
+ from ..question_check_box import QuestionCheckBox
6
+ from ..decorators import inject_exception
7
7
 
8
8
 
9
9
  class QuestionTopK(QuestionCheckBox):
@@ -70,7 +70,7 @@ class QuestionTopK(QuestionCheckBox):
70
70
 
71
71
  def main():
72
72
  """Test QuestionTopK."""
73
- from edsl.questions.derived.QuestionTopK import QuestionTopK
73
+ from edsl.questions import QuestionTopK
74
74
 
75
75
  q = QuestionTopK.example()
76
76
  q.question_text
@@ -1,9 +1,9 @@
1
1
  from __future__ import annotations
2
2
  from typing import Optional
3
- from edsl.questions.descriptors import QuestionOptionsDescriptor
4
- from edsl.questions.QuestionMultipleChoice import QuestionMultipleChoice
3
+ from ..descriptors import QuestionOptionsDescriptor
4
+ from ..question_multiple_choice import QuestionMultipleChoice
5
5
 
6
- from edsl.questions.decorators import inject_exception
6
+ from ..decorators import inject_exception
7
7
 
8
8
 
9
9
  class QuestionYesNo(QuestionMultipleChoice):
@@ -54,8 +54,7 @@ class QuestionYesNo(QuestionMultipleChoice):
54
54
 
55
55
  def main():
56
56
  """Create an example of a yes/no question and demonstrate its functionality."""
57
- from edsl.questions.derived.QuestionYesNo import QuestionYesNo
58
-
57
+ # Use the class directly since we're already in the module
59
58
  q = QuestionYesNo.example()
60
59
  q.question_text
61
60
  q.question_options
@@ -2,14 +2,12 @@
2
2
 
3
3
  from abc import ABC, abstractmethod
4
4
  import re
5
- import textwrap
6
5
  from typing import Any, Callable, List, Optional
7
- from edsl.exceptions.questions import (
6
+ from .exceptions import (
8
7
  QuestionCreationValidationError,
9
8
  QuestionAnswerValidationError,
10
9
  )
11
- from edsl.questions.settings import Settings
12
-
10
+ from .settings import Settings
13
11
 
14
12
  ################################
15
13
  # Helper functions
@@ -262,7 +260,7 @@ class QuestionOptionsDescriptor(BaseDescriptor):
262
260
  >>> _ = q_class(["a", "b", "c", "d", "d"])
263
261
  Traceback (most recent call last):
264
262
  ...
265
- edsl.exceptions.questions.QuestionCreationValidationError: Question options must be unique (got ['a', 'b', 'c', 'd', 'd']).
263
+ edsl.questions.exceptions.QuestionCreationValidationError: Question options must be unique (got ['a', 'b', 'c', 'd', 'd']).
266
264
 
267
265
  We allow dynamic question options, which are strings of the form '{{ question_options }}'.
268
266
 
@@ -270,7 +268,7 @@ class QuestionOptionsDescriptor(BaseDescriptor):
270
268
  >>> _ = q_class("dynamic_options")
271
269
  Traceback (most recent call last):
272
270
  ...
273
- edsl.exceptions.questions.QuestionCreationValidationError: ...
271
+ edsl.questions.exceptions.QuestionCreationValidationError: ...
274
272
  """
275
273
 
276
274
  @classmethod
@@ -391,10 +389,6 @@ class QuestionTextDescriptor(BaseDescriptor):
391
389
 
392
390
  >>> _ = TestQuestion("What is the capital of France?")
393
391
  >>> _ = TestQuestion("What is the capital of France? {{variable}}")
394
- >>> _ = TestQuestion("What is the capital of France? {{variable name}}")
395
- Traceback (most recent call last):
396
- ...
397
- edsl.exceptions.questions.QuestionCreationValidationError: Question text contains an invalid identifier: 'variable name'
398
392
  """
399
393
 
400
394
  def validate(self, value, instance):
@@ -408,21 +402,21 @@ class QuestionTextDescriptor(BaseDescriptor):
408
402
 
409
403
  #value = textwrap.dedent(value).strip()
410
404
 
411
- if contains_single_braced_substring(value):
412
- import warnings
413
-
414
- # # warnings.warn(
415
- # # f"WARNING: Question text contains a single-braced substring: If you intended to parameterize the question with a Scenario this should be changed to a double-braced substring, e.g. {{variable}}.\nSee details on constructing Scenarios in the docs: https://docs.expectedparrot.com/en/latest/scenarios.html",
416
- # # UserWarning,
417
- # # )
418
- warnings.warn(
419
- "WARNING: Question text contains a single-braced substring. "
420
- "If you intended to parameterize the question with a Scenario, this will "
421
- "be changed to a double-braced substring, e.g. {{variable}}.\n"
422
- "See details on constructing Scenarios in the docs: "
423
- "https://docs.expectedparrot.com/en/latest/scenarios.html",
424
- UserWarning,
425
- )
405
+ # if contains_single_braced_substring(value):
406
+ # import warnings
407
+
408
+ # # # warnings.warn(
409
+ # # # f"WARNING: Question text contains a single-braced substring: If you intended to parameterize the question with a Scenario this should be changed to a double-braced substring, e.g. {{variable}}.\nSee details on constructing Scenarios in the docs: https://docs.expectedparrot.com/en/latest/scenarios.html",
410
+ # # # UserWarning,
411
+ # # # )
412
+ # warnings.warn(
413
+ # "WARNING: Question text contains a single-braced substring. "
414
+ # "If you intended to parameterize the question with a Scenario, this will "
415
+ # "be changed to a double-braced substring, e.g. {{variable}}.\n"
416
+ # "See details on constructing Scenarios in the docs: "
417
+ # "https://docs.expectedparrot.com/en/latest/scenarios.html",
418
+ # UserWarning,
419
+ # )
426
420
  # Automatically replace single braces with double braces
427
421
  # This is here because if the user is using an f-string, the double brace will get converted to a single brace.
428
422
  # This undoes that.
@@ -430,11 +424,11 @@ class QuestionTextDescriptor(BaseDescriptor):
430
424
  return value
431
425
 
432
426
  # iterate through all doubles braces and check if they are valid python identifiers
433
- for match in re.finditer(r"\{\{([^\{\}]+)\}\}", value):
434
- if " " in match.group(1).strip():
435
- raise QuestionCreationValidationError(
436
- f"Question text contains an invalid identifier: '{match.group(1)}'"
437
- )
427
+ # for match in re.finditer(r"\{\{([^\{\}]+)\}\}", value):
428
+ # if " " in match.group(1).strip():
429
+ # raise QuestionCreationValidationError(
430
+ # f"Question text contains an invalid identifier: '{match.group(1)}'"
431
+ # )
438
432
 
439
433
  return None
440
434
 
@@ -1,13 +1,12 @@
1
1
  from typing import List, Any, Dict, Union
2
- from jinja2 import Environment
3
- from edsl.questions.QuestionBase import QuestionBase
4
- from edsl import ScenarioList
5
-
2
+ from jinja2 import Environment, Undefined
3
+ from .question_base import QuestionBase
4
+ from ..scenarios import ScenarioList
6
5
 
7
6
  class LoopProcessor:
8
7
  def __init__(self, question: QuestionBase):
9
8
  self.question = question
10
- self.env = Environment()
9
+ self.env = Environment(undefined=Undefined)
11
10
 
12
11
  def process_templates(self, scenario_list: ScenarioList) -> List[QuestionBase]:
13
12
  """Process templates for each scenario and return list of modified questions.
@@ -46,8 +45,11 @@ class LoopProcessor:
46
45
  """
47
46
  processed = {}
48
47
 
48
+ extended_scenario = scenario.copy()
49
+ extended_scenario.update({"scenario": scenario})
50
+
49
51
  for key, value in [(k, v) for k, v in data.items() if v is not None]:
50
- processed[key] = self._process_value(key, value, scenario)
52
+ processed[key] = self._process_value(key, value, extended_scenario)
51
53
 
52
54
  return processed
53
55
 
@@ -96,9 +98,61 @@ class LoopProcessor:
96
98
  scenario: Current scenario
97
99
 
98
100
  Returns:
99
- Rendered template string
101
+ Rendered template string, preserving any unmatched template variables
102
+
103
+ Examples:
104
+ >>> from edsl.questions import QuestionBase
105
+ >>> q = QuestionBase()
106
+ >>> q.question_text = "test"
107
+ >>> p = LoopProcessor(q)
108
+ >>> p._render_template("Hello {{name}}!", {"name": "World"})
109
+ 'Hello World!'
110
+
111
+ >>> p._render_template("{{a}} and {{b}}", {"b": 6})
112
+ '{{ a }} and 6'
113
+
114
+ >>> p._render_template("{{x}} + {{y}} = {{z}}", {"x": 2, "y": 3})
115
+ '2 + 3 = {{ z }}'
116
+
117
+ >>> p._render_template("No variables here", {})
118
+ 'No variables here'
119
+
120
+ >>> p._render_template("{{item.price}}", {"item": {"price": 9.99}})
121
+ '9.99'
122
+
123
+ >>> p._render_template("{{item.missing}}", {"item": {"price": 9.99}})
124
+ '{{ item.missing }}'
100
125
  """
101
- return self.env.from_string(template).render(scenario)
126
+ import re
127
+
128
+ # Regular expression to find Jinja2 variables in the template
129
+ pattern = r'(?P<open>\{\{\s*)(?P<var>[a-zA-Z0-9_.]+)(?P<close>\s*\}\})'
130
+
131
+ def replace_var(match):
132
+ var_name = match.group('var')
133
+ open_brace = match.group('open')
134
+ close_brace = match.group('close')
135
+
136
+ # Try to evaluate the variable in the context
137
+ try:
138
+ # Handle nested attributes (like item.price)
139
+ parts = var_name.split('.')
140
+ value = scenario
141
+ for part in parts:
142
+ if part in value:
143
+ value = value[part]
144
+ else:
145
+ # If any part doesn't exist, return the original with spacing
146
+ return f"{{ {var_name} }}".replace("{", "{{").replace("}", "}}")
147
+ # Return the rendered value if successful
148
+ return str(value)
149
+ except (KeyError, TypeError):
150
+ # Return the original variable name with the expected spacing
151
+ return f"{{ {var_name} }}".replace("{", "{{").replace("}", "}}")
152
+
153
+ # Replace all variables in the template
154
+ result = re.sub(pattern, replace_var, template)
155
+ return result
102
156
 
103
157
  def _process_list(self, items: List[Any], scenario: Dict[str, Any]) -> List[Any]:
104
158
  """Process all items in a list.
@@ -135,15 +189,7 @@ class LoopProcessor:
135
189
  }
136
190
 
137
191
 
138
- # Usage example:
139
- """
140
- from edsl import QuestionFreeText, ScenarioList
192
+ if __name__ == "__main__":
193
+ import doctest
141
194
 
142
- question = QuestionFreeText(
143
- question_text="What are your thoughts on: {{subject}}?",
144
- question_name="base_{{subject}}"
145
- )
146
- processor = TemplateProcessor(question)
147
- scenarios = ScenarioList.from_list("subject", ["Math", "Economics", "Chemistry"])
148
- processed_questions = processor.process_templates(scenarios)
149
- """
195
+ doctest.testmod()