edsl 0.1.15__py3-none-any.whl → 0.1.40__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 (407) hide show
  1. edsl/Base.py +348 -38
  2. edsl/BaseDiff.py +260 -0
  3. edsl/TemplateLoader.py +24 -0
  4. edsl/__init__.py +45 -10
  5. edsl/__version__.py +1 -1
  6. edsl/agents/Agent.py +842 -144
  7. edsl/agents/AgentList.py +521 -25
  8. edsl/agents/Invigilator.py +250 -374
  9. edsl/agents/InvigilatorBase.py +257 -0
  10. edsl/agents/PromptConstructor.py +272 -0
  11. edsl/agents/QuestionInstructionPromptBuilder.py +128 -0
  12. edsl/agents/QuestionTemplateReplacementsBuilder.py +137 -0
  13. edsl/agents/descriptors.py +43 -13
  14. edsl/agents/prompt_helpers.py +129 -0
  15. edsl/agents/question_option_processor.py +172 -0
  16. edsl/auto/AutoStudy.py +130 -0
  17. edsl/auto/StageBase.py +243 -0
  18. edsl/auto/StageGenerateSurvey.py +178 -0
  19. edsl/auto/StageLabelQuestions.py +125 -0
  20. edsl/auto/StagePersona.py +61 -0
  21. edsl/auto/StagePersonaDimensionValueRanges.py +88 -0
  22. edsl/auto/StagePersonaDimensionValues.py +74 -0
  23. edsl/auto/StagePersonaDimensions.py +69 -0
  24. edsl/auto/StageQuestions.py +74 -0
  25. edsl/auto/SurveyCreatorPipeline.py +21 -0
  26. edsl/auto/utilities.py +218 -0
  27. edsl/base/Base.py +279 -0
  28. edsl/config.py +115 -113
  29. edsl/conversation/Conversation.py +290 -0
  30. edsl/conversation/car_buying.py +59 -0
  31. edsl/conversation/chips.py +95 -0
  32. edsl/conversation/mug_negotiation.py +81 -0
  33. edsl/conversation/next_speaker_utilities.py +93 -0
  34. edsl/coop/CoopFunctionsMixin.py +15 -0
  35. edsl/coop/ExpectedParrotKeyHandler.py +125 -0
  36. edsl/coop/PriceFetcher.py +54 -0
  37. edsl/coop/__init__.py +1 -0
  38. edsl/coop/coop.py +1029 -134
  39. edsl/coop/utils.py +131 -0
  40. edsl/data/Cache.py +560 -89
  41. edsl/data/CacheEntry.py +230 -0
  42. edsl/data/CacheHandler.py +168 -0
  43. edsl/data/RemoteCacheSync.py +186 -0
  44. edsl/data/SQLiteDict.py +292 -0
  45. edsl/data/__init__.py +5 -3
  46. edsl/data/orm.py +6 -33
  47. edsl/data_transfer_models.py +74 -27
  48. edsl/enums.py +165 -8
  49. edsl/exceptions/BaseException.py +21 -0
  50. edsl/exceptions/__init__.py +52 -46
  51. edsl/exceptions/agents.py +33 -15
  52. edsl/exceptions/cache.py +5 -0
  53. edsl/exceptions/coop.py +8 -0
  54. edsl/exceptions/general.py +34 -0
  55. edsl/exceptions/inference_services.py +5 -0
  56. edsl/exceptions/jobs.py +15 -0
  57. edsl/exceptions/language_models.py +46 -1
  58. edsl/exceptions/questions.py +80 -5
  59. edsl/exceptions/results.py +16 -5
  60. edsl/exceptions/scenarios.py +29 -0
  61. edsl/exceptions/surveys.py +13 -10
  62. edsl/inference_services/AnthropicService.py +106 -0
  63. edsl/inference_services/AvailableModelCacheHandler.py +184 -0
  64. edsl/inference_services/AvailableModelFetcher.py +215 -0
  65. edsl/inference_services/AwsBedrock.py +118 -0
  66. edsl/inference_services/AzureAI.py +215 -0
  67. edsl/inference_services/DeepInfraService.py +18 -0
  68. edsl/inference_services/GoogleService.py +143 -0
  69. edsl/inference_services/GroqService.py +20 -0
  70. edsl/inference_services/InferenceServiceABC.py +80 -0
  71. edsl/inference_services/InferenceServicesCollection.py +138 -0
  72. edsl/inference_services/MistralAIService.py +120 -0
  73. edsl/inference_services/OllamaService.py +18 -0
  74. edsl/inference_services/OpenAIService.py +236 -0
  75. edsl/inference_services/PerplexityService.py +160 -0
  76. edsl/inference_services/ServiceAvailability.py +135 -0
  77. edsl/inference_services/TestService.py +90 -0
  78. edsl/inference_services/TogetherAIService.py +172 -0
  79. edsl/inference_services/data_structures.py +134 -0
  80. edsl/inference_services/models_available_cache.py +118 -0
  81. edsl/inference_services/rate_limits_cache.py +25 -0
  82. edsl/inference_services/registry.py +41 -0
  83. edsl/inference_services/write_available.py +10 -0
  84. edsl/jobs/AnswerQuestionFunctionConstructor.py +223 -0
  85. edsl/jobs/Answers.py +21 -20
  86. edsl/jobs/FetchInvigilator.py +47 -0
  87. edsl/jobs/InterviewTaskManager.py +98 -0
  88. edsl/jobs/InterviewsConstructor.py +50 -0
  89. edsl/jobs/Jobs.py +684 -206
  90. edsl/jobs/JobsChecks.py +172 -0
  91. edsl/jobs/JobsComponentConstructor.py +189 -0
  92. edsl/jobs/JobsPrompts.py +270 -0
  93. edsl/jobs/JobsRemoteInferenceHandler.py +311 -0
  94. edsl/jobs/JobsRemoteInferenceLogger.py +239 -0
  95. edsl/jobs/RequestTokenEstimator.py +30 -0
  96. edsl/jobs/async_interview_runner.py +138 -0
  97. edsl/jobs/buckets/BucketCollection.py +104 -0
  98. edsl/jobs/buckets/ModelBuckets.py +65 -0
  99. edsl/jobs/buckets/TokenBucket.py +283 -0
  100. edsl/jobs/buckets/TokenBucketAPI.py +211 -0
  101. edsl/jobs/buckets/TokenBucketClient.py +191 -0
  102. edsl/jobs/check_survey_scenario_compatibility.py +85 -0
  103. edsl/jobs/data_structures.py +120 -0
  104. edsl/jobs/decorators.py +35 -0
  105. edsl/jobs/interviews/Interview.py +392 -0
  106. edsl/jobs/interviews/InterviewExceptionCollection.py +99 -0
  107. edsl/jobs/interviews/InterviewExceptionEntry.py +186 -0
  108. edsl/jobs/interviews/InterviewStatistic.py +63 -0
  109. edsl/jobs/interviews/InterviewStatisticsCollection.py +25 -0
  110. edsl/jobs/interviews/InterviewStatusDictionary.py +78 -0
  111. edsl/jobs/interviews/InterviewStatusLog.py +92 -0
  112. edsl/jobs/interviews/ReportErrors.py +66 -0
  113. edsl/jobs/interviews/interview_status_enum.py +9 -0
  114. edsl/jobs/jobs_status_enums.py +9 -0
  115. edsl/jobs/loggers/HTMLTableJobLogger.py +304 -0
  116. edsl/jobs/results_exceptions_handler.py +98 -0
  117. edsl/jobs/runners/JobsRunnerAsyncio.py +151 -110
  118. edsl/jobs/runners/JobsRunnerStatus.py +298 -0
  119. edsl/jobs/tasks/QuestionTaskCreator.py +244 -0
  120. edsl/jobs/tasks/TaskCreators.py +64 -0
  121. edsl/jobs/tasks/TaskHistory.py +470 -0
  122. edsl/jobs/tasks/TaskStatusLog.py +23 -0
  123. edsl/jobs/tasks/task_status_enum.py +161 -0
  124. edsl/jobs/tokens/InterviewTokenUsage.py +27 -0
  125. edsl/jobs/tokens/TokenUsage.py +34 -0
  126. edsl/language_models/ComputeCost.py +63 -0
  127. edsl/language_models/LanguageModel.py +507 -386
  128. edsl/language_models/ModelList.py +164 -0
  129. edsl/language_models/PriceManager.py +127 -0
  130. edsl/language_models/RawResponseHandler.py +106 -0
  131. edsl/language_models/RegisterLanguageModelsMeta.py +184 -0
  132. edsl/language_models/__init__.py +1 -8
  133. edsl/language_models/fake_openai_call.py +15 -0
  134. edsl/language_models/fake_openai_service.py +61 -0
  135. edsl/language_models/key_management/KeyLookup.py +63 -0
  136. edsl/language_models/key_management/KeyLookupBuilder.py +273 -0
  137. edsl/language_models/key_management/KeyLookupCollection.py +38 -0
  138. edsl/language_models/key_management/__init__.py +0 -0
  139. edsl/language_models/key_management/models.py +131 -0
  140. edsl/language_models/model.py +256 -0
  141. edsl/language_models/repair.py +109 -41
  142. edsl/language_models/utilities.py +65 -0
  143. edsl/notebooks/Notebook.py +263 -0
  144. edsl/notebooks/NotebookToLaTeX.py +142 -0
  145. edsl/notebooks/__init__.py +1 -0
  146. edsl/prompts/Prompt.py +222 -93
  147. edsl/prompts/__init__.py +1 -1
  148. edsl/questions/ExceptionExplainer.py +77 -0
  149. edsl/questions/HTMLQuestion.py +103 -0
  150. edsl/questions/QuestionBase.py +518 -0
  151. edsl/questions/QuestionBasePromptsMixin.py +221 -0
  152. edsl/questions/QuestionBudget.py +164 -67
  153. edsl/questions/QuestionCheckBox.py +281 -62
  154. edsl/questions/QuestionDict.py +343 -0
  155. edsl/questions/QuestionExtract.py +136 -50
  156. edsl/questions/QuestionFreeText.py +79 -55
  157. edsl/questions/QuestionFunctional.py +138 -41
  158. edsl/questions/QuestionList.py +184 -57
  159. edsl/questions/QuestionMatrix.py +265 -0
  160. edsl/questions/QuestionMultipleChoice.py +293 -69
  161. edsl/questions/QuestionNumerical.py +109 -56
  162. edsl/questions/QuestionRank.py +244 -49
  163. edsl/questions/Quick.py +41 -0
  164. edsl/questions/SimpleAskMixin.py +74 -0
  165. edsl/questions/__init__.py +9 -6
  166. edsl/questions/{AnswerValidatorMixin.py → answer_validator_mixin.py} +153 -38
  167. edsl/questions/compose_questions.py +13 -7
  168. edsl/questions/data_structures.py +20 -0
  169. edsl/questions/decorators.py +21 -0
  170. edsl/questions/derived/QuestionLikertFive.py +28 -26
  171. edsl/questions/derived/QuestionLinearScale.py +41 -28
  172. edsl/questions/derived/QuestionTopK.py +34 -26
  173. edsl/questions/derived/QuestionYesNo.py +40 -27
  174. edsl/questions/descriptors.py +228 -74
  175. edsl/questions/loop_processor.py +149 -0
  176. edsl/questions/prompt_templates/question_budget.jinja +13 -0
  177. edsl/questions/prompt_templates/question_checkbox.jinja +32 -0
  178. edsl/questions/prompt_templates/question_extract.jinja +11 -0
  179. edsl/questions/prompt_templates/question_free_text.jinja +3 -0
  180. edsl/questions/prompt_templates/question_linear_scale.jinja +11 -0
  181. edsl/questions/prompt_templates/question_list.jinja +17 -0
  182. edsl/questions/prompt_templates/question_multiple_choice.jinja +33 -0
  183. edsl/questions/prompt_templates/question_numerical.jinja +37 -0
  184. edsl/questions/question_base_gen_mixin.py +168 -0
  185. edsl/questions/question_registry.py +130 -46
  186. edsl/questions/register_questions_meta.py +71 -0
  187. edsl/questions/response_validator_abc.py +188 -0
  188. edsl/questions/response_validator_factory.py +34 -0
  189. edsl/questions/settings.py +5 -2
  190. edsl/questions/templates/__init__.py +0 -0
  191. edsl/questions/templates/budget/__init__.py +0 -0
  192. edsl/questions/templates/budget/answering_instructions.jinja +7 -0
  193. edsl/questions/templates/budget/question_presentation.jinja +7 -0
  194. edsl/questions/templates/checkbox/__init__.py +0 -0
  195. edsl/questions/templates/checkbox/answering_instructions.jinja +10 -0
  196. edsl/questions/templates/checkbox/question_presentation.jinja +22 -0
  197. edsl/questions/templates/dict/__init__.py +0 -0
  198. edsl/questions/templates/dict/answering_instructions.jinja +21 -0
  199. edsl/questions/templates/dict/question_presentation.jinja +1 -0
  200. edsl/questions/templates/extract/__init__.py +0 -0
  201. edsl/questions/templates/extract/answering_instructions.jinja +7 -0
  202. edsl/questions/templates/extract/question_presentation.jinja +1 -0
  203. edsl/questions/templates/free_text/__init__.py +0 -0
  204. edsl/questions/templates/free_text/answering_instructions.jinja +0 -0
  205. edsl/questions/templates/free_text/question_presentation.jinja +1 -0
  206. edsl/questions/templates/likert_five/__init__.py +0 -0
  207. edsl/questions/templates/likert_five/answering_instructions.jinja +10 -0
  208. edsl/questions/templates/likert_five/question_presentation.jinja +12 -0
  209. edsl/questions/templates/linear_scale/__init__.py +0 -0
  210. edsl/questions/templates/linear_scale/answering_instructions.jinja +5 -0
  211. edsl/questions/templates/linear_scale/question_presentation.jinja +5 -0
  212. edsl/questions/templates/list/__init__.py +0 -0
  213. edsl/questions/templates/list/answering_instructions.jinja +4 -0
  214. edsl/questions/templates/list/question_presentation.jinja +5 -0
  215. edsl/questions/templates/matrix/__init__.py +1 -0
  216. edsl/questions/templates/matrix/answering_instructions.jinja +5 -0
  217. edsl/questions/templates/matrix/question_presentation.jinja +20 -0
  218. edsl/questions/templates/multiple_choice/__init__.py +0 -0
  219. edsl/questions/templates/multiple_choice/answering_instructions.jinja +9 -0
  220. edsl/questions/templates/multiple_choice/html.jinja +0 -0
  221. edsl/questions/templates/multiple_choice/question_presentation.jinja +12 -0
  222. edsl/questions/templates/numerical/__init__.py +0 -0
  223. edsl/questions/templates/numerical/answering_instructions.jinja +7 -0
  224. edsl/questions/templates/numerical/question_presentation.jinja +7 -0
  225. edsl/questions/templates/rank/__init__.py +0 -0
  226. edsl/questions/templates/rank/answering_instructions.jinja +11 -0
  227. edsl/questions/templates/rank/question_presentation.jinja +15 -0
  228. edsl/questions/templates/top_k/__init__.py +0 -0
  229. edsl/questions/templates/top_k/answering_instructions.jinja +8 -0
  230. edsl/questions/templates/top_k/question_presentation.jinja +22 -0
  231. edsl/questions/templates/yes_no/__init__.py +0 -0
  232. edsl/questions/templates/yes_no/answering_instructions.jinja +6 -0
  233. edsl/questions/templates/yes_no/question_presentation.jinja +12 -0
  234. edsl/results/CSSParameterizer.py +108 -0
  235. edsl/results/Dataset.py +550 -19
  236. edsl/results/DatasetExportMixin.py +594 -0
  237. edsl/results/DatasetTree.py +295 -0
  238. edsl/results/MarkdownToDocx.py +122 -0
  239. edsl/results/MarkdownToPDF.py +111 -0
  240. edsl/results/Result.py +477 -173
  241. edsl/results/Results.py +987 -269
  242. edsl/results/ResultsExportMixin.py +28 -125
  243. edsl/results/ResultsGGMixin.py +83 -15
  244. edsl/results/TableDisplay.py +125 -0
  245. edsl/results/TextEditor.py +50 -0
  246. edsl/results/__init__.py +1 -1
  247. edsl/results/file_exports.py +252 -0
  248. edsl/results/results_fetch_mixin.py +33 -0
  249. edsl/results/results_selector.py +145 -0
  250. edsl/results/results_tools_mixin.py +98 -0
  251. edsl/results/smart_objects.py +96 -0
  252. edsl/results/table_data_class.py +12 -0
  253. edsl/results/table_display.css +78 -0
  254. edsl/results/table_renderers.py +118 -0
  255. edsl/results/tree_explore.py +115 -0
  256. edsl/scenarios/ConstructDownloadLink.py +109 -0
  257. edsl/scenarios/DocumentChunker.py +102 -0
  258. edsl/scenarios/DocxScenario.py +16 -0
  259. edsl/scenarios/FileStore.py +543 -0
  260. edsl/scenarios/PdfExtractor.py +40 -0
  261. edsl/scenarios/Scenario.py +431 -62
  262. edsl/scenarios/ScenarioHtmlMixin.py +65 -0
  263. edsl/scenarios/ScenarioList.py +1415 -45
  264. edsl/scenarios/ScenarioListExportMixin.py +45 -0
  265. edsl/scenarios/ScenarioListPdfMixin.py +239 -0
  266. edsl/scenarios/__init__.py +2 -0
  267. edsl/scenarios/directory_scanner.py +96 -0
  268. edsl/scenarios/file_methods.py +85 -0
  269. edsl/scenarios/handlers/__init__.py +13 -0
  270. edsl/scenarios/handlers/csv.py +49 -0
  271. edsl/scenarios/handlers/docx.py +76 -0
  272. edsl/scenarios/handlers/html.py +37 -0
  273. edsl/scenarios/handlers/json.py +111 -0
  274. edsl/scenarios/handlers/latex.py +5 -0
  275. edsl/scenarios/handlers/md.py +51 -0
  276. edsl/scenarios/handlers/pdf.py +68 -0
  277. edsl/scenarios/handlers/png.py +39 -0
  278. edsl/scenarios/handlers/pptx.py +105 -0
  279. edsl/scenarios/handlers/py.py +294 -0
  280. edsl/scenarios/handlers/sql.py +313 -0
  281. edsl/scenarios/handlers/sqlite.py +149 -0
  282. edsl/scenarios/handlers/txt.py +33 -0
  283. edsl/scenarios/scenario_join.py +131 -0
  284. edsl/scenarios/scenario_selector.py +156 -0
  285. edsl/shared.py +1 -0
  286. edsl/study/ObjectEntry.py +173 -0
  287. edsl/study/ProofOfWork.py +113 -0
  288. edsl/study/SnapShot.py +80 -0
  289. edsl/study/Study.py +521 -0
  290. edsl/study/__init__.py +4 -0
  291. edsl/surveys/ConstructDAG.py +92 -0
  292. edsl/surveys/DAG.py +92 -11
  293. edsl/surveys/EditSurvey.py +221 -0
  294. edsl/surveys/InstructionHandler.py +100 -0
  295. edsl/surveys/Memory.py +9 -4
  296. edsl/surveys/MemoryManagement.py +72 -0
  297. edsl/surveys/MemoryPlan.py +156 -35
  298. edsl/surveys/Rule.py +221 -74
  299. edsl/surveys/RuleCollection.py +241 -61
  300. edsl/surveys/RuleManager.py +172 -0
  301. edsl/surveys/Simulator.py +75 -0
  302. edsl/surveys/Survey.py +1079 -339
  303. edsl/surveys/SurveyCSS.py +273 -0
  304. edsl/surveys/SurveyExportMixin.py +235 -40
  305. edsl/surveys/SurveyFlowVisualization.py +181 -0
  306. edsl/surveys/SurveyQualtricsImport.py +284 -0
  307. edsl/surveys/SurveyToApp.py +141 -0
  308. edsl/surveys/__init__.py +4 -2
  309. edsl/surveys/base.py +19 -3
  310. edsl/surveys/descriptors.py +17 -6
  311. edsl/surveys/instructions/ChangeInstruction.py +48 -0
  312. edsl/surveys/instructions/Instruction.py +56 -0
  313. edsl/surveys/instructions/InstructionCollection.py +82 -0
  314. edsl/surveys/instructions/__init__.py +0 -0
  315. edsl/templates/error_reporting/base.html +24 -0
  316. edsl/templates/error_reporting/exceptions_by_model.html +35 -0
  317. edsl/templates/error_reporting/exceptions_by_question_name.html +17 -0
  318. edsl/templates/error_reporting/exceptions_by_type.html +17 -0
  319. edsl/templates/error_reporting/interview_details.html +116 -0
  320. edsl/templates/error_reporting/interviews.html +19 -0
  321. edsl/templates/error_reporting/overview.html +5 -0
  322. edsl/templates/error_reporting/performance_plot.html +2 -0
  323. edsl/templates/error_reporting/report.css +74 -0
  324. edsl/templates/error_reporting/report.html +118 -0
  325. edsl/templates/error_reporting/report.js +25 -0
  326. edsl/tools/__init__.py +1 -0
  327. edsl/tools/clusters.py +192 -0
  328. edsl/tools/embeddings.py +27 -0
  329. edsl/tools/embeddings_plotting.py +118 -0
  330. edsl/tools/plotting.py +112 -0
  331. edsl/tools/summarize.py +18 -0
  332. edsl/utilities/PrettyList.py +56 -0
  333. edsl/utilities/SystemInfo.py +5 -0
  334. edsl/utilities/__init__.py +21 -20
  335. edsl/utilities/ast_utilities.py +3 -0
  336. edsl/utilities/data/Registry.py +2 -0
  337. edsl/utilities/decorators.py +41 -0
  338. edsl/utilities/gcp_bucket/__init__.py +0 -0
  339. edsl/utilities/gcp_bucket/cloud_storage.py +96 -0
  340. edsl/utilities/interface.py +310 -60
  341. edsl/utilities/is_notebook.py +18 -0
  342. edsl/utilities/is_valid_variable_name.py +11 -0
  343. edsl/utilities/naming_utilities.py +263 -0
  344. edsl/utilities/remove_edsl_version.py +24 -0
  345. edsl/utilities/repair_functions.py +28 -0
  346. edsl/utilities/restricted_python.py +70 -0
  347. edsl/utilities/utilities.py +203 -13
  348. edsl-0.1.40.dist-info/METADATA +111 -0
  349. edsl-0.1.40.dist-info/RECORD +362 -0
  350. {edsl-0.1.15.dist-info → edsl-0.1.40.dist-info}/WHEEL +1 -1
  351. edsl/agents/AgentListExportMixin.py +0 -24
  352. edsl/coop/old.py +0 -31
  353. edsl/data/Database.py +0 -141
  354. edsl/data/crud.py +0 -121
  355. edsl/jobs/Interview.py +0 -435
  356. edsl/jobs/JobsRunner.py +0 -63
  357. edsl/jobs/JobsRunnerStatusMixin.py +0 -115
  358. edsl/jobs/base.py +0 -47
  359. edsl/jobs/buckets.py +0 -178
  360. edsl/jobs/runners/JobsRunnerDryRun.py +0 -19
  361. edsl/jobs/runners/JobsRunnerStreaming.py +0 -54
  362. edsl/jobs/task_management.py +0 -215
  363. edsl/jobs/token_tracking.py +0 -78
  364. edsl/language_models/DeepInfra.py +0 -69
  365. edsl/language_models/OpenAI.py +0 -98
  366. edsl/language_models/model_interfaces/GeminiPro.py +0 -66
  367. edsl/language_models/model_interfaces/LanguageModelOpenAIFour.py +0 -8
  368. edsl/language_models/model_interfaces/LanguageModelOpenAIThreeFiveTurbo.py +0 -8
  369. edsl/language_models/model_interfaces/LlamaTwo13B.py +0 -21
  370. edsl/language_models/model_interfaces/LlamaTwo70B.py +0 -21
  371. edsl/language_models/model_interfaces/Mixtral8x7B.py +0 -24
  372. edsl/language_models/registry.py +0 -81
  373. edsl/language_models/schemas.py +0 -15
  374. edsl/language_models/unused/ReplicateBase.py +0 -83
  375. edsl/prompts/QuestionInstructionsBase.py +0 -6
  376. edsl/prompts/library/agent_instructions.py +0 -29
  377. edsl/prompts/library/agent_persona.py +0 -17
  378. edsl/prompts/library/question_budget.py +0 -26
  379. edsl/prompts/library/question_checkbox.py +0 -32
  380. edsl/prompts/library/question_extract.py +0 -19
  381. edsl/prompts/library/question_freetext.py +0 -14
  382. edsl/prompts/library/question_linear_scale.py +0 -20
  383. edsl/prompts/library/question_list.py +0 -22
  384. edsl/prompts/library/question_multiple_choice.py +0 -44
  385. edsl/prompts/library/question_numerical.py +0 -31
  386. edsl/prompts/library/question_rank.py +0 -21
  387. edsl/prompts/prompt_config.py +0 -33
  388. edsl/prompts/registry.py +0 -185
  389. edsl/questions/Question.py +0 -240
  390. edsl/report/InputOutputDataTypes.py +0 -134
  391. edsl/report/RegressionMixin.py +0 -28
  392. edsl/report/ReportOutputs.py +0 -1228
  393. edsl/report/ResultsFetchMixin.py +0 -106
  394. edsl/report/ResultsOutputMixin.py +0 -14
  395. edsl/report/demo.ipynb +0 -645
  396. edsl/results/ResultsDBMixin.py +0 -184
  397. edsl/surveys/SurveyFlowVisualizationMixin.py +0 -92
  398. edsl/trackers/Tracker.py +0 -91
  399. edsl/trackers/TrackerAPI.py +0 -196
  400. edsl/trackers/TrackerTasks.py +0 -70
  401. edsl/utilities/pastebin.py +0 -141
  402. edsl-0.1.15.dist-info/METADATA +0 -69
  403. edsl-0.1.15.dist-info/RECORD +0 -142
  404. /edsl/{language_models/model_interfaces → inference_services}/__init__.py +0 -0
  405. /edsl/{report/__init__.py → jobs/runners/JobsRunnerStatusData.py} +0 -0
  406. /edsl/{trackers/__init__.py → language_models/ServiceDataSources.py} +0 -0
  407. {edsl-0.1.15.dist-info → edsl-0.1.40.dist-info}/LICENSE +0 -0
@@ -1,42 +1,68 @@
1
- from typing import List, Union, Any
2
- from collections import defaultdict, UserList
1
+ """A collection of rules for a survey."""
3
2
 
4
- from edsl.exceptions import (
3
+ from typing import List, Union, Any, Optional
4
+ from collections import defaultdict, UserList, namedtuple
5
+
6
+ from edsl.exceptions.surveys import (
5
7
  SurveyRuleCannotEvaluateError,
6
8
  SurveyRuleCollectionHasNoRulesAtNodeError,
7
9
  )
8
- from edsl.utilities.interface import print_table_with_rich
10
+
9
11
  from edsl.surveys.Rule import Rule
10
12
  from edsl.surveys.base import EndOfSurvey
11
13
  from edsl.surveys.DAG import DAG
12
14
 
13
- from graphlib import TopologicalSorter
14
-
15
- from collections import namedtuple
16
15
 
17
16
  NextQuestion = namedtuple(
18
17
  "NextQuestion", "next_q, num_rules_found, expressions_evaluating_to_true, priority"
19
18
  )
20
19
 
21
- ## We're going to need the survey object itself
22
- ## so we know how long the survey is, unless we move
23
-
24
20
 
25
21
  class RuleCollection(UserList):
26
- "A collection of rules for a particular survey"
22
+ """A collection of rules for a particular survey."""
23
+
24
+ def __init__(self, num_questions: Optional[int] = None, rules: List[Rule] = None):
25
+ """Initialize the RuleCollection object.
27
26
 
28
- def __init__(self, num_questions: int = None, rules: List[Rule] = None):
27
+ :param num_questions: The number of questions in the survey.
28
+ :param rules: A list of Rule objects.
29
+ """
29
30
  super().__init__(rules or [])
30
31
  self.num_questions = num_questions
31
32
 
32
33
  def __repr__(self):
33
- """
34
- >>> rule_collection = RuleCollection.example()
35
- >>> _ = eval(repr(rule_collection))
34
+ """Return a string representation of the RuleCollection object.
35
+
36
+ Example usage:
37
+
38
+ .. code-block:: python
39
+
40
+ rule_collection = RuleCollection.example()
41
+ _ = eval(repr(rule_collection))
42
+
36
43
  """
37
44
  return f"RuleCollection(rules={self.data}, num_questions={self.num_questions})"
38
45
 
39
- def to_dict(self):
46
+ def to_dataset(self):
47
+ """Return a Dataset object representation of the RuleCollection object."""
48
+ from edsl.results.Dataset import Dataset
49
+
50
+ keys = ["current_q", "expression", "next_q", "priority", "before_rule"]
51
+ rule_list = {}
52
+ for rule in sorted(self, key=lambda r: r.current_q):
53
+ for k in keys:
54
+ rule_list.setdefault(k, []).append(getattr(rule, k))
55
+
56
+ return Dataset([{k: v} for k, v in rule_list.items()])
57
+
58
+ def _repr_html_(self):
59
+ """Return an HTML representation of the RuleCollection object."""
60
+ from edsl.results.Dataset import Dataset
61
+
62
+ return self.to_dataset()._repr_html_()
63
+
64
+ def to_dict(self, add_edsl_version=True):
65
+ """Create a dictionary representation of the RuleCollection object."""
40
66
  return {
41
67
  "rules": [rule.to_dict() for rule in self],
42
68
  "num_questions": self.num_questions,
@@ -44,6 +70,14 @@ class RuleCollection(UserList):
44
70
 
45
71
  @classmethod
46
72
  def from_dict(cls, rule_collection_dict):
73
+ """Create a RuleCollection object from a dictionary.
74
+
75
+ >>> rule_collection = RuleCollection.example()
76
+ >>> rule_collection_dict = rule_collection.to_dict()
77
+ >>> new_rule_collection = RuleCollection.from_dict(rule_collection_dict)
78
+ >>> repr(new_rule_collection) == repr(rule_collection)
79
+ True
80
+ """
47
81
  rules = [
48
82
  Rule.from_dict(rule_dict) for rule_dict in rule_collection_dict["rules"]
49
83
  ]
@@ -52,44 +86,119 @@ class RuleCollection(UserList):
52
86
  new_rc.num_questions = num_questions
53
87
  return new_rc
54
88
 
55
- def add_rule(self, rule: Rule):
56
- """Adds a rule to a survey. If it's not, return human-readable complaints"""
89
+ def add_rule(self, rule: Rule) -> None:
90
+ """Add a rule to a survey.
91
+
92
+ >>> rule_collection = RuleCollection()
93
+ >>> rule_collection.add_rule(Rule(current_q=1, expression="q1 == 'yes'", next_q=3, priority=1, question_name_to_index={'q1': 1, 'q2': 2, 'q3': 3, 'q4': 4}))
94
+ >>> len(rule_collection)
95
+ 1
96
+
97
+ >>> rule_collection = RuleCollection()
98
+ >>> r = Rule(current_q=1, expression="True", next_q=3, priority=1, question_name_to_index={'q1': 1, 'q2': 2, 'q3': 3, 'q4': 4}, before_rule = True)
99
+ >>> rule_collection.add_rule(r)
100
+ >>> rule_collection[0] == r
101
+ True
102
+ >>> len(rule_collection.applicable_rules(1, before_rule=True))
103
+ 1
104
+ >>> len(rule_collection.applicable_rules(1, before_rule=False))
105
+ 0
106
+ """
57
107
  self.append(rule)
58
108
 
59
109
  def show_rules(self) -> None:
60
- keys = ["current_q", "expression", "next_q", "priority"]
61
- rule_list = []
62
- for rule in sorted(self, key=lambda r: r.current_q):
63
- rule_list.append({k: getattr(rule, k) for k in keys})
110
+ """Print the rules in a table.
111
+
112
+
113
+ .. code-block:: python
114
+
115
+ rule_collection = RuleCollection.example()
116
+ rule_collection.show_rules()
117
+ ┏━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━┓
118
+ ┃ current_q ┃ expression ┃ next_q ┃ priority ┃ before_rule ┃
119
+ ┡━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━┩
120
+ │ 1 │ q1 == 'yes' │ 3 │ 1 │ False │
121
+ │ 1 │ q1 == 'no' │ 2 │ 1 │ False │
122
+ └───────────┴─────────────┴────────┴──────────┴─────────────┘
123
+ """
124
+ return self.to_dataset()
64
125
 
65
- print_table_with_rich(rule_list)
126
+ def skip_question_before_running(self, q_now: int, answers: dict[str, Any]) -> bool:
127
+ """Determine if a question should be skipped before running the question.
66
128
 
67
- def applicable_rules(self, q_now: int) -> list:
68
- """Which rules apply at the current node?
129
+ :param q_now: The current question index.
130
+ :param answers: The answers to the survey questions.
131
+
132
+ >>> rule_collection = RuleCollection()
133
+ >>> r = Rule(current_q=1, expression="True", next_q=2, priority=1, question_name_to_index={}, before_rule = True)
134
+ >>> rule_collection.add_rule(r)
135
+ >>> rule_collection.skip_question_before_running(1, {})
136
+ True
137
+
138
+ >>> rule_collection = RuleCollection()
139
+ >>> r = Rule(current_q=1, expression="False", next_q=2, priority=1, question_name_to_index={}, before_rule = True)
140
+ >>> rule_collection.add_rule(r)
141
+ >>> rule_collection.skip_question_before_running(1, {})
142
+ False
143
+
144
+ """
145
+ for rule in self.applicable_rules(q_now, before_rule=True):
146
+ if rule.evaluate(answers):
147
+ return True
148
+ return False
149
+
150
+ def applicable_rules(self, q_now: int, before_rule: bool = False) -> list:
151
+ """Show the rules that apply at the current node.
152
+
153
+ :param q_now: The current question index.
154
+ :param before_rule: If True, return rules that are of the type that apply before the question is asked.
155
+
156
+ Example usage:
69
157
 
70
158
  >>> rule_collection = RuleCollection.example()
71
159
  >>> rule_collection.applicable_rules(1)
72
- [Rule(current_q=1, expression="q1 == 'yes'", next_q=3, priority=1, question_name_to_index={'q1': 1, 'q2': 2, 'q3': 3, 'q4': 4}), Rule(current_q=1, expression="q1 == 'no'", next_q=2, priority=1, question_name_to_index={'q1': 1, 'q2': 2, 'q3': 3, 'q4': 4})]
160
+ [Rule(current_q=1, expression="q1 == 'yes'", next_q=3, priority=1, question_name_to_index={'q1': 1, 'q2': 2, 'q3': 3, 'q4': 4}, before_rule=False), Rule(current_q=1, expression="q1 == 'no'", next_q=2, priority=1, question_name_to_index={'q1': 1, 'q2': 2, 'q3': 3, 'q4': 4}, before_rule=False)]
161
+
162
+ The default is that the rule is applied after the question is asked.
163
+ If we want to see the rules that apply before the question is asked, we can set before_rule=True.
164
+
165
+ .. code-block:: python
73
166
 
74
- More than one rule can apply. E.g., suppose we are at node 1.
167
+ rule_collection = RuleCollection.example()
168
+ rule_collection.applicable_rules(1)
169
+ [Rule(current_q=1, expression="q1 == 'yes'", next_q=3, priority=1, question_name_to_index={'q1': 1, 'q2': 2, 'q3': 3, 'q4': 4}), Rule(current_q=1, expression="q1 == 'no'", next_q=2, priority=1, question_name_to_index={'q1': 1, 'q2': 2, 'q3': 3, 'q4': 4})]
170
+
171
+ More than one rule can apply. For example, suppose we are at node 1.
75
172
  We could have three rules:
76
173
  1. "q1 == 'a' ==> 3
77
174
  2. "q1 == 'b' ==> 4
78
175
  3. "q1 == 'c' ==> 5
79
176
  """
80
- return [rule for rule in self if rule.current_q == q_now]
177
+ return [
178
+ rule
179
+ for rule in self
180
+ if rule.current_q == q_now and rule.before_rule == before_rule
181
+ ]
81
182
 
82
183
  def next_question(self, q_now: int, answers: dict[str, Any]) -> NextQuestion:
83
- """Find the next question by index, given the rule collection"""
84
- # what rules apply at the current node?
184
+ """Find the next question by index, given the rule collection.
185
+
186
+ This rule is applied after the question is answered.
85
187
 
86
- # tracking
188
+ :param q_now: The current question index.
189
+ :param answers: The answers to the survey questions so far, including the current question.
190
+
191
+ >>> rule_collection = RuleCollection.example()
192
+ >>> rule_collection.next_question(1, {'q1': 'yes'})
193
+ NextQuestion(next_q=3, num_rules_found=2, expressions_evaluating_to_true=1, priority=1)
194
+
195
+ """
87
196
  expressions_evaluating_to_true = 0
88
197
  next_q = None
89
198
  highest_priority = -2 # start with -2 to 'pick up' the default rule added
90
199
  num_rules_found = 0
91
200
 
92
- for rule in self.applicable_rules(q_now):
201
+ for rule in self.applicable_rules(q_now, before_rule=False):
93
202
  num_rules_found += 1
94
203
  try:
95
204
  if rule.evaluate(answers): # evaluates to True
@@ -105,36 +214,55 @@ class RuleCollection(UserList):
105
214
  f"No rules found for question {q_now}"
106
215
  )
107
216
 
217
+ ## Now we need to check if the *next question* has any 'before; rules that we should follow
218
+ for rule in self.applicable_rules(next_q, before_rule=True):
219
+ if rule.evaluate(answers): # rule evaluates to True
220
+ return self.next_question(next_q, answers)
221
+
108
222
  return NextQuestion(
109
223
  next_q, num_rules_found, expressions_evaluating_to_true, highest_priority
110
224
  )
111
225
 
112
226
  @property
113
227
  def non_default_rules(self) -> List[Rule]:
114
- """Returns all rules that are not the default rule"
228
+ """Return all rules that are not the default rule.
229
+
115
230
  >>> rule_collection = RuleCollection.example()
116
231
  >>> len(rule_collection.non_default_rules)
117
232
  2
233
+
234
+ Example usage:
235
+
236
+ .. code-block:: python
237
+
238
+ rule_collection = RuleCollection.example()
239
+ len(rule_collection.non_default_rules)
240
+ 2
241
+
118
242
  """
119
243
  return [rule for rule in self if rule.priority > -1]
120
244
 
121
245
  def keys_between(self, start_q, end_q, right_inclusive=True):
122
- """Returns a list of all question indices between start_q and end_q
123
- >>> rule_collection = RuleCollection(num_questions=5)
124
- >>> rule_collection.keys_between(1, 3)
125
- [2, 3]
126
- >>> rule_collection.keys_between(1, 4)
127
- [2, 3, 4]
128
- >>> rule_collection.keys_between(1, EndOfSurvey, right_inclusive=False)
129
- [2, 3]
130
- """
246
+ """Return a list of all question indices between start_q and end_q.
131
247
 
132
- # If it's the end of the survey, all questions between the start_q and the end of the survey
133
- # now depend on the start_q
248
+ Example usage:
249
+
250
+ .. code-block:: python
251
+
252
+ rule_collection = RuleCollection(num_questions=5)
253
+ rule_collection.keys_between(1, 3)
254
+ [2, 3]
255
+ rule_collection.keys_between(1, 4)
256
+ [2, 3, 4]
257
+ rule_collection.keys_between(1, EndOfSurvey, right_inclusive=False)
258
+ [2, 3]
259
+
260
+ """
261
+ # If it's the end of the survey, all questions between the start_q and the end of the survey now depend on the start_q
134
262
  if end_q == EndOfSurvey:
135
263
  if self.num_questions is None:
136
264
  raise ValueError(
137
- "Cannot determine DAG when EndOfSurvey and when num_questions is not known"
265
+ "Cannot determine DAG when EndOfSurvey and when num_questions is not known."
138
266
  )
139
267
  end_q = self.num_questions - 1
140
268
 
@@ -145,7 +273,8 @@ class RuleCollection(UserList):
145
273
  @property
146
274
  def dag(self) -> dict:
147
275
  """
148
- Finds the DAG of the survey, based on the skip logic.
276
+ Find the DAG of the survey, based on the skip logic.
277
+
149
278
  Keys are children questions; the list of values are nodes that must be answered first
150
279
 
151
280
  Rules are designated at the current question and then direct where
@@ -154,28 +283,78 @@ class RuleCollection(UserList):
154
283
  the current and destination nodes are also included as keys, as they will depend
155
284
  on the answer to the focal node as well.
156
285
 
157
- ## If we have a rule that says "if q1 == 'yes', go to q3",
158
- ## Then q3 depends on q1, but so does q2
159
- ## So the DAG would be {3: [1], 2: [1]}
286
+ For exmaple, if we have a rule that says "if q1 == 'yes', go to q3", then q3 depends on q1, but so does q2.
287
+ So the DAG would be {3: [1], 2: [1]}.
288
+
289
+ Example usage:
290
+
291
+ .. code-block:: python
292
+
293
+ rule_collection = RuleCollection(num_questions=5)
294
+ qn2i = {'q1': 1, 'q2': 2, 'q3': 3, 'q4': 4}
295
+ rule_collection.add_rule(Rule(current_q=1, expression="q1 == 'yes'", next_q=3, priority=1, question_name_to_index = qn2i))
296
+ rule_collection.add_rule(Rule(current_q=1, expression="q1 == 'no'", next_q=2, priority=1, question_name_to_index = qn2i))
297
+ rule_collection.dag
298
+ {2: {1}, 3: {1}}
160
299
 
161
- >>> rule_collection = RuleCollection(num_questions=5)
162
- >>> qn2i = {'q1': 1, 'q2': 2, 'q3': 3, 'q4': 4}
163
- >>> rule_collection.add_rule(Rule(current_q=1, expression="q1 == 'yes'", next_q=3, priority=1, question_name_to_index = qn2i))
164
- >>> rule_collection.add_rule(Rule(current_q=1, expression="q1 == 'no'", next_q=2, priority=1, question_name_to_index = qn2i))
165
- >>> rule_collection.dag
166
- {2: {1}, 3: {1}}
167
300
  """
168
301
  children_to_parents = defaultdict(set)
169
- # we are only interested in non-default rules. Default rules are those
302
+ # We are only interested in non-default rules. Default rules are those
170
303
  # that just go to the next question, so they don't add any dependencies
304
+
305
+ ## I think for a skip-question, the potenially-skippable question
306
+ ## depends on all the other questions bein answered first.
171
307
  for rule in self.non_default_rules:
172
- current_q, next_q = rule.current_q, rule.next_q
173
- for q in self.keys_between(current_q, next_q):
174
- children_to_parents[q].add(current_q)
308
+ if not rule.before_rule:
309
+ # for a regular rule, the next question depends on the current question answer
310
+ current_q, next_q = rule.current_q, rule.next_q
311
+ for q in self.keys_between(current_q, next_q):
312
+ children_to_parents[q].add(current_q)
313
+ else:
314
+ # for the 'before rule' skipping depends on all previous answers.
315
+ focal_q = rule.current_q
316
+ for q in range(0, focal_q):
317
+ children_to_parents[focal_q].add(q)
318
+
175
319
  return DAG(dict(sorted(children_to_parents.items())))
176
320
 
321
+ def detect_cycles(self):
322
+ """
323
+ Detect cycles in the survey rules using depth-first search.
324
+
325
+ :return: A list of cycles if any are found, otherwise an empty list.
326
+ """
327
+ dag = self.dag
328
+ visited = set()
329
+ path = []
330
+ cycles = []
331
+
332
+ def dfs(node):
333
+ if node in path:
334
+ cycle = path[path.index(node) :]
335
+ cycles.append(cycle + [node])
336
+ return
337
+
338
+ if node in visited:
339
+ return
340
+
341
+ visited.add(node)
342
+ path.append(node)
343
+
344
+ for child in dag.get(node, []):
345
+ dfs(child)
346
+
347
+ path.pop()
348
+
349
+ for node in dag:
350
+ if node not in visited:
351
+ dfs(node)
352
+
353
+ return cycles
354
+
177
355
  @classmethod
178
356
  def example(cls):
357
+ """Create an example RuleCollection object."""
179
358
  qn2i = {"q1": 1, "q2": 2, "q3": 3, "q4": 4}
180
359
  return cls(
181
360
  num_questions=5,
@@ -199,7 +378,8 @@ class RuleCollection(UserList):
199
378
 
200
379
 
201
380
  if __name__ == "__main__":
202
- # pass
203
381
  import doctest
204
382
 
205
- doctest.testmod()
383
+ doctest.testmod(optionflags=doctest.ELLIPSIS)
384
+
385
+ print(RuleCollection.example()._repr_html_())
@@ -0,0 +1,172 @@
1
+ from typing import Union, TYPE_CHECKING
2
+
3
+ if TYPE_CHECKING:
4
+ from edsl.questions.QuestionBase import QuestionBase
5
+
6
+ from edsl.surveys.Rule import Rule
7
+ from .base import RulePriority, EndOfSurvey
8
+ from edsl.exceptions.surveys import SurveyError, SurveyCreationError
9
+
10
+
11
+ class ValidatedString(str):
12
+ def __new__(cls, content):
13
+ if "<>" in content:
14
+ raise SurveyCreationError(
15
+ "The expression contains '<>', which is not allowed. You probably mean '!='."
16
+ )
17
+ return super().__new__(cls, content)
18
+
19
+
20
+ class RuleManager:
21
+ def __init__(self, survey):
22
+ self.survey = survey
23
+
24
+ def _get_question_index(
25
+ self, q: Union["QuestionBase", str, EndOfSurvey.__class__]
26
+ ) -> Union[int, EndOfSurvey.__class__]:
27
+ """Return the index of the question or EndOfSurvey object.
28
+
29
+ :param q: The question or question name to get the index of.
30
+
31
+ It can handle it if the user passes in the question name, the question object, or the EndOfSurvey object.
32
+
33
+ >>> from edsl.questions import QuestionFreeText
34
+ >>> from edsl import Survey
35
+ >>> s = Survey.example()
36
+ >>> s._get_question_index("q0")
37
+ 0
38
+
39
+ This doesnt' work with questions that don't exist:
40
+
41
+ >>> s._get_question_index("poop")
42
+ Traceback (most recent call last):
43
+ ...
44
+ edsl.exceptions.surveys.SurveyError: Question name poop not found in survey. The current question names are {'q0': 0, 'q1': 1, 'q2': 2}.
45
+ ...
46
+ """
47
+ if q == EndOfSurvey:
48
+ return EndOfSurvey
49
+ else:
50
+ question_name = q if isinstance(q, str) else q.question_name
51
+ if question_name not in self.survey.question_name_to_index:
52
+ raise SurveyError(
53
+ f"""Question name {question_name} not found in survey. The current question names are {self.survey.question_name_to_index}."""
54
+ )
55
+ return self.survey.question_name_to_index[question_name]
56
+
57
+ def _get_new_rule_priority(
58
+ self, question_index: int, before_rule: bool = False
59
+ ) -> int:
60
+ """Return the priority for the new rule.
61
+
62
+ :param question_index: The index of the question to add the rule to.
63
+ :param before_rule: Whether the rule is evaluated before the question is answered.
64
+
65
+ >>> from edsl import Survey
66
+ >>> s = Survey.example()
67
+ >>> RuleManager(s)._get_new_rule_priority(0)
68
+ 1
69
+ """
70
+ current_priorities = [
71
+ rule.priority
72
+ for rule in self.survey.rule_collection.applicable_rules(
73
+ question_index, before_rule
74
+ )
75
+ ]
76
+ if len(current_priorities) == 0:
77
+ return RulePriority.DEFAULT.value + 1
78
+
79
+ max_priority = max(current_priorities)
80
+ # newer rules take priority over older rules
81
+ new_priority = (
82
+ RulePriority.DEFAULT.value
83
+ if len(current_priorities) == 0
84
+ else max_priority + 1
85
+ )
86
+ return new_priority
87
+
88
+ def add_rule(
89
+ self,
90
+ question: Union["QuestionBase", str],
91
+ expression: str,
92
+ next_question: Union["QuestionBase", str, int],
93
+ before_rule: bool = False,
94
+ ) -> "Survey":
95
+ """
96
+ Add a rule to a Question of the Survey with the appropriate priority.
97
+
98
+ :param question: The question to add the rule to.
99
+ :param expression: The expression to evaluate.
100
+ :param next_question: The next question to go to if the rule is true.
101
+ :param before_rule: Whether the rule is evaluated before the question is answered.
102
+
103
+
104
+ - The last rule added for the question will have the highest priority.
105
+ - If there are no rules, the rule added gets priority -1.
106
+ """
107
+ question_index = self.survey._get_question_index(question) # Fix
108
+
109
+ # Might not have the name of the next question yet
110
+ if isinstance(next_question, int):
111
+ next_question_index = next_question
112
+ else:
113
+ next_question_index = self._get_question_index(next_question)
114
+
115
+ new_priority = self._get_new_rule_priority(question_index, before_rule) # fix
116
+
117
+ self.survey.rule_collection.add_rule(
118
+ Rule(
119
+ current_q=question_index,
120
+ expression=expression,
121
+ next_q=next_question_index,
122
+ question_name_to_index=self.survey.question_name_to_index,
123
+ priority=new_priority,
124
+ before_rule=before_rule,
125
+ )
126
+ )
127
+
128
+ return self.survey
129
+
130
+ def add_stop_rule(
131
+ self, question: Union["QuestionBase", str], expression: str
132
+ ) -> "Survey":
133
+ """Add a rule that stops the survey.
134
+ The rule is evaluated *after* the question is answered. If the rule is true, the survey ends.
135
+
136
+ :param question: The question to add the stop rule to.
137
+ :param expression: The expression to evaluate.
138
+
139
+ If this rule is true, the survey ends.
140
+
141
+ Here, answering "yes" to q0 ends the survey:
142
+
143
+ >>> from edsl import Survey
144
+ >>> s = Survey.example().add_stop_rule("q0", "q0 == 'yes'")
145
+ >>> s.next_question("q0", {"q0": "yes"})
146
+ EndOfSurvey
147
+
148
+ By comparison, answering "no" to q0 does not end the survey:
149
+
150
+ >>> s.next_question("q0", {"q0": "no"}).question_name
151
+ 'q1'
152
+
153
+ >>> s.add_stop_rule("q0", "q1 <> 'yes'")
154
+ Traceback (most recent call last):
155
+ ...
156
+ edsl.exceptions.surveys.SurveyCreationError: The expression contains '<>', which is not allowed. You probably mean '!='.
157
+ ...
158
+ """
159
+ expression = ValidatedString(expression)
160
+ prior_question_appears = False
161
+ for prior_question in self.survey.questions:
162
+ if prior_question.question_name in expression:
163
+ prior_question_appears = True
164
+
165
+ if not prior_question_appears:
166
+ import warnings
167
+
168
+ warnings.warn(
169
+ f"The expression {expression} does not contain any prior question names. This is probably a mistake."
170
+ )
171
+ self.survey.add_rule(question, expression, EndOfSurvey)
172
+ return self.survey
@@ -0,0 +1,75 @@
1
+ from typing import Callable
2
+
3
+
4
+ class Simulator:
5
+ def __init__(self, survey):
6
+ self.survey = survey
7
+
8
+ @classmethod
9
+ def random_survey(cls):
10
+ """Create a random survey."""
11
+ from edsl.questions import QuestionMultipleChoice, QuestionFreeText
12
+ from random import choice
13
+ from edsl.surveys.Survey import Survey
14
+
15
+ num_questions = 10
16
+ questions = []
17
+ for i in range(num_questions):
18
+ if choice([True, False]):
19
+ q = QuestionMultipleChoice(
20
+ question_text="nothing",
21
+ question_name="q_" + str(i),
22
+ question_options=list(range(3)),
23
+ )
24
+ questions.append(q)
25
+ else:
26
+ questions.append(
27
+ QuestionFreeText(
28
+ question_text="nothing", question_name="q_" + str(i)
29
+ )
30
+ )
31
+ s = Survey(questions)
32
+ start_index = choice(range(num_questions - 1))
33
+ end_index = choice(range(start_index + 1, 10))
34
+ s = s.add_rule(f"q_{start_index}", "True", f"q_{end_index}")
35
+ question_to_delete = choice(range(num_questions))
36
+ s.delete_question(f"q_{question_to_delete}")
37
+ return s
38
+
39
+ def simulate(self) -> dict:
40
+ """Simulate the survey and return the answers."""
41
+ i = self.survey.gen_path_through_survey()
42
+ q = next(i)
43
+ num_passes = 0
44
+ while True:
45
+ num_passes += 1
46
+ try:
47
+ answer = q._simulate_answer()
48
+ q = i.send({q.question_name: answer["answer"]})
49
+ except StopIteration:
50
+ break
51
+
52
+ if num_passes > 100:
53
+ print("Too many passes.")
54
+ raise Exception("Too many passes.")
55
+ return self.survey.answers
56
+
57
+ def create_agent(self) -> "Agent":
58
+ """Create an agent from the simulated answers."""
59
+ answers_dict = self.survey.simulate()
60
+ from edsl.agents.Agent import Agent
61
+
62
+ def construct_answer_dict_function(traits: dict) -> Callable:
63
+ def func(self, question: "QuestionBase", scenario=None):
64
+ return traits.get(question.question_name, None)
65
+
66
+ return func
67
+
68
+ return Agent(traits=answers_dict).add_direct_question_answering_method(
69
+ construct_answer_dict_function(answers_dict)
70
+ )
71
+
72
+ def simulate_results(self) -> "Results":
73
+ """Simulate the survey and return the results."""
74
+ a = self.create_agent()
75
+ return self.survey.by([a]).run()