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
edsl/surveys/Survey.py CHANGED
@@ -1,41 +1,99 @@
1
+ """A Survey is collection of questions that can be administered to an Agent."""
2
+
1
3
  from __future__ import annotations
2
4
  import re
3
- from rich import print
4
- from rich.table import Table
5
- from rich.console import Console
6
- import io
7
-
8
- from dataclasses import dataclass
5
+ import random
6
+
7
+ from typing import (
8
+ Any,
9
+ Generator,
10
+ Optional,
11
+ Union,
12
+ List,
13
+ Literal,
14
+ Callable,
15
+ TYPE_CHECKING,
16
+ )
17
+ from uuid import uuid4
18
+ from edsl.Base import Base
19
+ from edsl.exceptions.surveys import SurveyCreationError, SurveyHasNoRulesError
20
+ from edsl.exceptions.surveys import SurveyError
9
21
  from collections import UserDict
10
22
 
11
- from typing import Any, Generator, Optional, Union, List
12
- from edsl.exceptions import SurveyCreationError, SurveyHasNoRulesError
13
- from edsl.questions.Question import Question
14
- from edsl.surveys.base import RulePriority, EndOfSurvey
15
- from edsl.surveys.Rule import Rule
16
- from edsl.surveys.RuleCollection import RuleCollection
17
23
 
18
- from edsl.Base import Base
19
- from edsl.surveys.SurveyExportMixin import SurveyExportMixin
20
- from edsl.surveys.descriptors import QuestionsDescriptor
21
- from edsl.surveys.MemoryPlan import MemoryPlan
24
+ class PseudoIndices(UserDict):
25
+ @property
26
+ def max_pseudo_index(self) -> float:
27
+ """Return the maximum pseudo index in the survey.
28
+ >>> Survey.example()._pseudo_indices.max_pseudo_index
29
+ 2
30
+ """
31
+ if len(self) == 0:
32
+ return -1
33
+ return max(self.values())
22
34
 
23
- from edsl.surveys.DAG import DAG
35
+ @property
36
+ def last_item_was_instruction(self) -> bool:
37
+ """Return whether the last item added to the survey was an instruction.
24
38
 
39
+ This is used to determine the pseudo-index of the next item added to the survey.
25
40
 
26
- @dataclass
27
- class SurveyMetaData:
28
- name: str = None
29
- description: str = None
30
- version: str = None
41
+ Example:
31
42
 
43
+ >>> s = Survey.example()
44
+ >>> s._pseudo_indices.last_item_was_instruction
45
+ False
46
+ >>> from edsl.surveys.instructions.Instruction import Instruction
47
+ >>> s = s.add_instruction(Instruction(text="Pay attention to the following questions.", name="intro"))
48
+ >>> s._pseudo_indices.last_item_was_instruction
49
+ True
50
+ """
51
+ return isinstance(self.max_pseudo_index, float)
32
52
 
33
- from edsl.surveys.SurveyFlowVisualizationMixin import SurveyFlowVisualizationMixin
34
53
 
54
+ if TYPE_CHECKING:
55
+ from edsl.questions.QuestionBase import QuestionBase
56
+ from edsl.agents.Agent import Agent
57
+ from edsl.surveys.DAG import DAG
58
+ from edsl.language_models.LanguageModel import LanguageModel
59
+ from edsl.scenarios.Scenario import Scenario
60
+ from edsl.data.Cache import Cache
35
61
 
36
- class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
37
- questions = QuestionsDescriptor()
62
+ # This is a hack to get around the fact that TypeAlias is not available in typing until Python 3.10
63
+ try:
64
+ from typing import TypeAlias
65
+ except ImportError:
66
+ from typing import _GenericAlias as TypeAlias
38
67
 
68
+ QuestionType: TypeAlias = Union[QuestionBase, Instruction, ChangeInstruction]
69
+ QuestionGroupType: TypeAlias = dict[str, tuple[int, int]]
70
+
71
+
72
+ from edsl.utilities.remove_edsl_version import remove_edsl_version
73
+
74
+ from edsl.surveys.instructions.InstructionCollection import InstructionCollection
75
+ from edsl.surveys.instructions.Instruction import Instruction
76
+ from edsl.surveys.instructions.ChangeInstruction import ChangeInstruction
77
+
78
+ from edsl.surveys.base import EndOfSurvey
79
+ from edsl.surveys.descriptors import QuestionsDescriptor
80
+ from edsl.surveys.MemoryPlan import MemoryPlan
81
+ from edsl.surveys.RuleCollection import RuleCollection
82
+ from edsl.surveys.SurveyExportMixin import SurveyExportMixin
83
+ from edsl.surveys.SurveyFlowVisualization import SurveyFlowVisualization
84
+ from edsl.surveys.InstructionHandler import InstructionHandler
85
+ from edsl.surveys.EditSurvey import EditSurvey
86
+ from edsl.surveys.Simulator import Simulator
87
+ from edsl.surveys.MemoryManagement import MemoryManagement
88
+ from edsl.surveys.RuleManager import RuleManager
89
+
90
+
91
+ class Survey(SurveyExportMixin, Base):
92
+ """A collection of questions that supports skip logic."""
93
+
94
+ __documentation__ = """https://docs.expectedparrot.com/en/latest/surveys.html"""
95
+
96
+ questions = QuestionsDescriptor()
39
97
  """
40
98
  A collection of questions that supports skip logic.
41
99
 
@@ -53,247 +111,911 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
53
111
 
54
112
  def __init__(
55
113
  self,
56
- questions: list[Question] = None,
57
- memory_plan: MemoryPlan = None,
58
- name: str = None,
59
- description: str = None,
60
- version: str = None,
114
+ questions: Optional[List["QuestionType"]] = None,
115
+ memory_plan: Optional["MemoryPlan"] = None,
116
+ rule_collection: Optional["RuleCollection"] = None,
117
+ question_groups: Optional["QuestionGroupType"] = None,
118
+ name: Optional[str] = None,
119
+ questions_to_randomize: Optional[List[str]] = None,
61
120
  ):
62
- """Creates a new survey."""
121
+ """Create a new survey.
122
+
123
+ :param questions: The questions in the survey.
124
+ :param memory_plan: The memory plan for the survey.
125
+ :param rule_collection: The rule collection for the survey.
126
+ :param question_groups: The groups of questions in the survey.
127
+ :param name: The name of the survey - DEPRECATED.
128
+
129
+
130
+ >>> from edsl import QuestionFreeText
131
+ >>> q1 = QuestionFreeText(question_text = "What is your name?", question_name = "name")
132
+ >>> q2 = QuestionFreeText(question_text = "What is your favorite color?", question_name = "color")
133
+ >>> q3 = QuestionFreeText(question_text = "Is a hot dog a sandwich", question_name = "food")
134
+ >>> s = Survey([q1, q2, q3], question_groups = {"demographics": (0, 1), "substantive":(3)})
135
+
136
+
137
+ """
138
+
139
+ self.raw_passed_questions = questions
140
+
141
+ true_questions = self._process_raw_questions(self.raw_passed_questions)
142
+
63
143
  self.rule_collection = RuleCollection(
64
- num_questions=len(questions) if questions else None
65
- )
66
- self.meta_data = SurveyMetaData(
67
- name=name, description=description, version=version
144
+ num_questions=len(true_questions) if true_questions else None
68
145
  )
69
- self.questions = questions or []
146
+ # the RuleCollection needs to be present while we add the questions; we might override this later
147
+ # if a rule_collection is provided. This allows us to serialize the survey with the rule_collection.
148
+
149
+ # this is where the Questions constructor is called.
150
+ self.questions = true_questions
151
+ # self.instruction_names_to_instructions = instruction_names_to_instructions
152
+
70
153
  self.memory_plan = memory_plan or MemoryPlan(self)
154
+ if question_groups is not None:
155
+ self.question_groups = question_groups
156
+ else:
157
+ self.question_groups = {}
71
158
 
159
+ # if a rule collection is provided, use it instead of the constructed one
160
+ if rule_collection is not None:
161
+ self.rule_collection = rule_collection
162
+
163
+ if name is not None:
164
+ import warnings
165
+
166
+ warnings.warn("name parameter to a survey is deprecated.")
167
+
168
+ if questions_to_randomize is not None:
169
+ self.questions_to_randomize = questions_to_randomize
170
+ else:
171
+ self.questions_to_randomize = []
172
+
173
+ self._seed = None
174
+
175
+ def draw(self) -> "Survey":
176
+ """Return a new survey with a randomly selected permutation of the options."""
177
+ if self._seed is None: # only set once
178
+ self._seed = hash(self)
179
+ random.seed(self._seed)
180
+
181
+ if len(self.questions_to_randomize) == 0:
182
+ return self
183
+
184
+ new_questions = []
185
+ for question in self.questions:
186
+ if question.question_name in self.questions_to_randomize:
187
+ new_questions.append(question.draw())
188
+ else:
189
+ new_questions.append(question.duplicate())
190
+
191
+ d = self.to_dict()
192
+ d["questions"] = [q.to_dict() for q in new_questions]
193
+ return Survey.from_dict(d)
194
+
195
+ def _process_raw_questions(self, questions: Optional[List["QuestionType"]]) -> list:
196
+ """Process the raw questions passed to the survey."""
197
+ handler = InstructionHandler(self)
198
+ components = handler.separate_questions_and_instructions(questions or [])
199
+ self._instruction_names_to_instructions = (
200
+ components.instruction_names_to_instructions
201
+ )
202
+ self._pseudo_indices = PseudoIndices(components.pseudo_indices)
203
+ return components.true_questions
204
+
205
+ # region: Survey instruction handling
72
206
  @property
73
- def name(self):
74
- # print("WARNING: name is deprecated. Please use meta_data.name instead.")
75
- return self.meta_data.name
207
+ def _relevant_instructions_dict(self) -> InstructionCollection:
208
+ """Return a dictionary with keys as question names and values as instructions that are relevant to the question.
209
+
210
+ >>> s = Survey.example(include_instructions=True)
211
+ >>> s._relevant_instructions_dict
212
+ {'q0': [Instruction(name="attention", text="Please pay attention!")], 'q1': [Instruction(name="attention", text="Please pay attention!")], 'q2': [Instruction(name="attention", text="Please pay attention!")]}
76
213
 
77
- def get_question(self, question_name) -> Question:
78
- """Returns the question object given the question name"""
214
+ """
215
+ return InstructionCollection(
216
+ self._instruction_names_to_instructions, self.questions
217
+ )
218
+
219
+ def _relevant_instructions(self, question: QuestionBase) -> dict:
220
+ """This should be a dictionry with keys as question names and values as instructions that are relevant to the question.
221
+
222
+ :param question: The question to get the relevant instructions for.
223
+
224
+ # Did the instruction come before the question and was it not modified by a change instruction?
225
+
226
+ """
227
+ return InstructionCollection(
228
+ self._instruction_names_to_instructions, self.questions
229
+ )[question]
230
+
231
+ def show_flow(self, filename: Optional[str] = None) -> None:
232
+ """Show the flow of the survey."""
233
+ SurveyFlowVisualization(self).show_flow(filename=filename)
234
+
235
+ def add_instruction(
236
+ self, instruction: Union["Instruction", "ChangeInstruction"]
237
+ ) -> Survey:
238
+ """
239
+ Add an instruction to the survey.
240
+
241
+ :param instruction: The instruction to add to the survey.
242
+
243
+ >>> from edsl import Instruction
244
+ >>> i = Instruction(text="Pay attention to the following questions.", name="intro")
245
+ >>> s = Survey().add_instruction(i)
246
+ >>> s._instruction_names_to_instructions
247
+ {'intro': Instruction(name="intro", text="Pay attention to the following questions.")}
248
+ >>> s._pseudo_indices
249
+ {'intro': -0.5}
250
+ """
251
+ return EditSurvey(self).add_instruction(instruction)
252
+
253
+ # endregion
254
+ @classmethod
255
+ def random_survey(cls):
256
+ return Simulator.random_survey()
257
+
258
+ def simulate(self) -> dict:
259
+ """Simulate the survey and return the answers."""
260
+ return Simulator(self).simulate()
261
+
262
+ # endregion
263
+
264
+ # region: Access methods
265
+ def _get_question_index(
266
+ self, q: Union[QuestionBase, str, EndOfSurvey.__class__]
267
+ ) -> Union[int, EndOfSurvey.__class__]:
268
+ """Return the index of the question or EndOfSurvey object.
269
+
270
+ :param q: The question or question name to get the index of.
271
+
272
+ It can handle it if the user passes in the question name, the question object, or the EndOfSurvey object.
273
+
274
+ >>> s = Survey.example()
275
+ >>> s._get_question_index("q0")
276
+ 0
277
+
278
+ This doesnt' work with questions that don't exist:
279
+
280
+ >>> s._get_question_index("poop")
281
+ Traceback (most recent call last):
282
+ ...
283
+ edsl.exceptions.surveys.SurveyError: Question name poop not found in survey. The current question names are {'q0': 0, 'q1': 1, 'q2': 2}.
284
+ ...
285
+ """
286
+ if q == EndOfSurvey:
287
+ return EndOfSurvey
288
+ else:
289
+ question_name = q if isinstance(q, str) else q.question_name
290
+ if question_name not in self.question_name_to_index:
291
+ raise SurveyError(
292
+ f"""Question name {question_name} not found in survey. The current question names are {self.question_name_to_index}."""
293
+ )
294
+ return self.question_name_to_index[question_name]
295
+
296
+ def _get_question_by_name(self, question_name: str) -> QuestionBase:
297
+ """
298
+ Return the question object given the question name.
299
+
300
+ :param question_name: The name of the question to get.
301
+
302
+ >>> s = Survey.example()
303
+ >>> s._get_question_by_name("q0")
304
+ Question('multiple_choice', question_name = \"""q0\""", question_text = \"""Do you like school?\""", question_options = ['yes', 'no'])
305
+ """
79
306
  if question_name not in self.question_name_to_index:
80
- raise KeyError(f"Question name {question_name} not found in survey.")
81
- index = self.question_name_to_index[question_name]
82
- return self._questions[index]
307
+ raise SurveyError(f"Question name {question_name} not found in survey.")
308
+ return self._questions[self.question_name_to_index[question_name]]
309
+
310
+ def question_names_to_questions(self) -> dict:
311
+ """Return a dictionary mapping question names to question attributes."""
312
+ return {q.question_name: q for q in self.questions}
83
313
 
84
314
  @property
85
315
  def question_names(self) -> list[str]:
86
- """Returns a list of question names in the survey
316
+ """Return a list of question names in the survey.
317
+
318
+ Example:
319
+
87
320
  >>> s = Survey.example()
88
321
  >>> s.question_names
89
322
  ['q0', 'q1', 'q2']
90
323
  """
91
- # return list(self.question_name_to_index.keys())
92
324
  return [q.question_name for q in self.questions]
93
325
 
94
326
  @property
95
327
  def question_name_to_index(self) -> dict[str, int]:
96
- """Returns a dictionary mapping question names to question indices
328
+ """Return a dictionary mapping question names to question indices.
329
+
330
+ Example:
331
+
97
332
  >>> s = Survey.example()
98
333
  >>> s.question_name_to_index
99
334
  {'q0': 0, 'q1': 1, 'q2': 2}
100
335
  """
101
336
  return {q.question_name: i for i, q in enumerate(self.questions)}
102
337
 
103
- def add_question(
104
- self, question: Question, question_name: Optional[str] = None
105
- ) -> Survey:
338
+ # endregion
339
+
340
+ # region: serialization methods
341
+ def to_dict(self, add_edsl_version=True) -> dict[str, Any]:
342
+ """Serialize the Survey object to a dictionary.
343
+
344
+ >>> s = Survey.example()
345
+ >>> s.to_dict(add_edsl_version = False).keys()
346
+ dict_keys(['questions', 'memory_plan', 'rule_collection', 'question_groups'])
347
+ """
348
+ from edsl import __version__
349
+
350
+ d = {
351
+ "questions": [
352
+ q.to_dict(add_edsl_version=add_edsl_version)
353
+ for q in self._recombined_questions_and_instructions()
354
+ ],
355
+ "memory_plan": self.memory_plan.to_dict(add_edsl_version=add_edsl_version),
356
+ "rule_collection": self.rule_collection.to_dict(
357
+ add_edsl_version=add_edsl_version
358
+ ),
359
+ "question_groups": self.question_groups,
360
+ }
361
+ if self.questions_to_randomize != []:
362
+ d["questions_to_randomize"] = self.questions_to_randomize
363
+
364
+ if add_edsl_version:
365
+ d["edsl_version"] = __version__
366
+ d["edsl_class_name"] = "Survey"
367
+ return d
368
+
369
+ @classmethod
370
+ @remove_edsl_version
371
+ def from_dict(cls, data: dict) -> Survey:
372
+ """Deserialize the dictionary back to a Survey object.
373
+
374
+ :param data: The dictionary to deserialize.
375
+
376
+ >>> d = Survey.example().to_dict()
377
+ >>> s = Survey.from_dict(d)
378
+ >>> s == Survey.example()
379
+ True
380
+
381
+ >>> s = Survey.example(include_instructions = True)
382
+ >>> d = s.to_dict()
383
+ >>> news = Survey.from_dict(d)
384
+ >>> news == s
385
+ True
386
+
106
387
  """
107
- Adds a question to survey.
108
- - The question is appended at the end of the self.questions list
109
- - A default rule is created that the next index is the next question.
388
+
389
+ def get_class(pass_dict):
390
+ from edsl.questions.QuestionBase import QuestionBase
391
+
392
+ if (class_name := pass_dict.get("edsl_class_name")) == "QuestionBase":
393
+ return QuestionBase
394
+ elif pass_dict.get("edsl_class_name") == "QuestionDict":
395
+ from edsl.questions.QuestionDict import QuestionDict
396
+
397
+ return QuestionDict
398
+ elif class_name == "Instruction":
399
+ from edsl.surveys.instructions.Instruction import Instruction
400
+
401
+ return Instruction
402
+ elif class_name == "ChangeInstruction":
403
+ from edsl.surveys.instructions.ChangeInstruction import (
404
+ ChangeInstruction,
405
+ )
406
+
407
+ return ChangeInstruction
408
+ else:
409
+ return QuestionBase
410
+
411
+ questions = [
412
+ get_class(q_dict).from_dict(q_dict) for q_dict in data["questions"]
413
+ ]
414
+ memory_plan = MemoryPlan.from_dict(data["memory_plan"])
415
+ if "questions_to_randomize" in data:
416
+ questions_to_randomize = data["questions_to_randomize"]
417
+ else:
418
+ questions_to_randomize = None
419
+ survey = cls(
420
+ questions=questions,
421
+ memory_plan=memory_plan,
422
+ rule_collection=RuleCollection.from_dict(data["rule_collection"]),
423
+ question_groups=data["question_groups"],
424
+ questions_to_randomize=questions_to_randomize,
425
+ )
426
+ return survey
427
+
428
+ # endregion
429
+
430
+ # region: Survey template parameters
431
+ @property
432
+ def scenario_attributes(self) -> list[str]:
433
+ """Return a list of attributes that admissible Scenarios should have.
434
+
435
+ Here we have a survey with a question that uses a jinja2 style {{ }} template:
436
+
437
+ >>> from edsl import QuestionFreeText
438
+ >>> s = Survey().add_question(QuestionFreeText(question_text="{{ greeting }}. What is your name?", question_name="name"))
439
+ >>> s.scenario_attributes
440
+ ['greeting']
441
+
442
+ >>> s = Survey().add_question(QuestionFreeText(question_text="{{ greeting }}. What is your {{ attribute }}?", question_name="name"))
443
+ >>> s.scenario_attributes
444
+ ['greeting', 'attribute']
445
+
446
+
110
447
  """
111
- if question_name is not None:
112
- print(
113
- "Warning: question_name is deprecated. Please use question.question_name instead."
114
- )
448
+ temp = []
449
+ for question in self.questions:
450
+ question_text = question.question_text
451
+ # extract the contents of all {{ }} in the question text using regex
452
+ matches = re.findall(r"\{\{(.+?)\}\}", question_text)
453
+ # remove whitespace
454
+ matches = [match.strip() for match in matches]
455
+ # add them to the temp list
456
+ temp.extend(matches)
457
+ return temp
115
458
 
116
- if question.question_name in self.question_names:
459
+ @property
460
+ def parameters(self):
461
+ """Return a set of parameters in the survey.
462
+
463
+ >>> s = Survey.example()
464
+ >>> s.parameters
465
+ set()
466
+ """
467
+ return set.union(*[q.parameters for q in self.questions])
468
+
469
+ @property
470
+ def parameters_by_question(self):
471
+ """Return a dictionary of parameters by question in the survey.
472
+ >>> from edsl import QuestionFreeText
473
+ >>> q = QuestionFreeText(question_name = "example", question_text = "What is the capital of {{ country}}?")
474
+ >>> s = Survey([q])
475
+ >>> s.parameters_by_question
476
+ {'example': {'country'}}
477
+ """
478
+ return {q.question_name: q.parameters for q in self.questions}
479
+
480
+ # endregion
481
+
482
+ # region: Survey construction
483
+
484
+ # region: Adding questions and combining surveys
485
+ def __add__(self, other: Survey) -> Survey:
486
+ """Combine two surveys.
487
+
488
+ :param other: The other survey to combine with this one.
489
+ >>> s1 = Survey.example()
490
+ >>> from edsl import QuestionFreeText
491
+ >>> s2 = Survey([QuestionFreeText(question_text="What is your name?", question_name="yo")])
492
+ >>> s3 = s1 + s2
493
+ Traceback (most recent call last):
494
+ ...
495
+ edsl.exceptions.surveys.SurveyCreationError: ...
496
+ ...
497
+ >>> s3 = s1.clear_non_default_rules() + s2
498
+ >>> len(s3.questions)
499
+ 4
500
+
501
+ """
502
+ if (
503
+ len(self.rule_collection.non_default_rules) > 0
504
+ or len(other.rule_collection.non_default_rules) > 0
505
+ ):
117
506
  raise SurveyCreationError(
118
- f"""Question name already exists in survey. Please use a different name for the offensing question.
119
- The problemetic question name is {question_name}.
120
- """
121
- )
122
- index = len(self.questions)
123
- # TODO: This is a bit ugly because the user
124
- # doesn't "know" about _questions - it's generated by the
125
- # descriptor.
126
- self._questions.append(question)
127
-
128
- # using index + 1 presumes there is a next question
129
- self.rule_collection.add_rule(
130
- Rule(
131
- current_q=index,
132
- expression="True",
133
- next_q=index + 1,
134
- question_name_to_index=self.question_name_to_index,
135
- priority=RulePriority.DEFAULT.value,
507
+ "Cannot combine two surveys with non-default rules. Please use the 'clear_non_default_rules' method to remove non-default rules from the survey.",
136
508
  )
509
+
510
+ return Survey(questions=self.questions + other.questions)
511
+
512
+ def move_question(self, identifier: Union[str, int], new_index: int) -> Survey:
513
+ """
514
+ >>> from edsl import QuestionMultipleChoice, Survey
515
+ >>> s = Survey.example()
516
+ >>> s.question_names
517
+ ['q0', 'q1', 'q2']
518
+ >>> s.move_question("q0", 2).question_names
519
+ ['q1', 'q2', 'q0']
520
+ """
521
+ return EditSurvey(self).move_question(identifier, new_index)
522
+
523
+ def delete_question(self, identifier: Union[str, int]) -> Survey:
524
+ """
525
+ Delete a question from the survey.
526
+
527
+ :param identifier: The name or index of the question to delete.
528
+ :return: The updated Survey object.
529
+
530
+ >>> from edsl import QuestionMultipleChoice, Survey
531
+ >>> q1 = QuestionMultipleChoice(question_text="Q1", question_options=["A", "B"], question_name="q1")
532
+ >>> q2 = QuestionMultipleChoice(question_text="Q2", question_options=["C", "D"], question_name="q2")
533
+ >>> s = Survey().add_question(q1).add_question(q2)
534
+ >>> _ = s.delete_question("q1")
535
+ >>> len(s.questions)
536
+ 1
537
+ >>> _ = s.delete_question(0)
538
+ >>> len(s.questions)
539
+ 0
540
+ """
541
+ return EditSurvey(self).delete_question(identifier)
542
+
543
+ def add_question(
544
+ self, question: QuestionBase, index: Optional[int] = None
545
+ ) -> Survey:
546
+ """
547
+ Add a question to survey.
548
+
549
+ :param question: The question to add to the survey.
550
+ :param question_name: The name of the question. If not provided, the question name is used.
551
+
552
+ The question is appended at the end of the self.questions list
553
+ A default rule is created that the next index is the next question.
554
+
555
+ >>> from edsl import QuestionMultipleChoice
556
+ >>> q = QuestionMultipleChoice(question_text = "Do you like school?", question_options=["yes", "no"], question_name="q0")
557
+ >>> s = Survey().add_question(q)
558
+
559
+ >>> s = Survey().add_question(q).add_question(q)
560
+ Traceback (most recent call last):
561
+ ...
562
+ edsl.exceptions.surveys.SurveyCreationError: Question name 'q0' already exists in survey. Existing names are ['q0'].
563
+ ...
564
+ """
565
+ return EditSurvey(self).add_question(question, index)
566
+
567
+ def _recombined_questions_and_instructions(
568
+ self,
569
+ ) -> list[Union[QuestionBase, "Instruction"]]:
570
+ """Return a list of questions and instructions sorted by pseudo index."""
571
+ questions_and_instructions = self._questions + list(
572
+ self._instruction_names_to_instructions.values()
573
+ )
574
+ return sorted(
575
+ questions_and_instructions, key=lambda x: self._pseudo_indices[x.name]
137
576
  )
138
577
 
139
- # a question might be added before the memory plan is created
140
- # it's ok because the memory plan will be updated when it is created
141
- if hasattr(self, "memory_plan"):
142
- self.memory_plan.add_question(question)
578
+ # endregion
579
+
580
+ # region: Memory plan methods
581
+ def set_full_memory_mode(self) -> Survey:
582
+ """Add instructions to a survey that the agent should remember all of the answers to the questions in the survey.
143
583
 
584
+ >>> s = Survey.example().set_full_memory_mode()
585
+
586
+ """
587
+ MemoryManagement(self)._set_memory_plan(lambda i: self.question_names[:i])
144
588
  return self
145
589
 
146
- def set_full_memory_mode(self) -> None:
147
- """This adds instructions to a survey that the agent should remember all of the answers to the questions in the survey."""
148
- self._set_memory_plan(lambda i: self.question_names[:i])
590
+ def set_lagged_memory(self, lags: int) -> Survey:
591
+ """Add instructions to a survey that the agent should remember the answers to the questions in the survey.
149
592
 
150
- def set_lagged_memory(self, lags: int):
151
- """This adds instructions to a survey that the agent should remember the answers to the questions in the survey.
152
593
  The agent should remember the answers to the questions in the survey from the previous lags.
153
594
  """
154
- self._set_memory_plan(lambda i: self.question_names[max(0, i - lags) : i])
595
+ MemoryManagement(self)._set_memory_plan(
596
+ lambda i: self.question_names[max(0, i - lags) : i]
597
+ )
598
+ return self
155
599
 
156
- def _set_memory_plan(self, prior_questions_func):
157
- """Helper method to set memory plan based on a provided function determining prior questions."""
158
- for i, question_name in enumerate(self.question_names):
159
- self.memory_plan.add_memory_collection(
160
- focal_question=question_name,
161
- prior_questions=prior_questions_func(i),
162
- )
600
+ def _set_memory_plan(self, prior_questions_func: Callable) -> None:
601
+ """Set memory plan based on a provided function determining prior questions.
602
+
603
+ :param prior_questions_func: A function that takes the index of the current question and returns a list of prior questions to remember.
604
+
605
+ >>> s = Survey.example()
606
+ >>> s._set_memory_plan(lambda i: s.question_names[:i])
163
607
 
164
- def add_targeted_memory(
165
- self, focal_question: Union[Question, str], prior_question: Union[Question, str]
166
- ) -> None:
167
- """This adds instructions to a survey than when answering focal_question,
168
- the agent should also remember the answers to prior_questions listed in prior_questions.
169
608
  """
170
- focal_question_name = self.question_names[
171
- self._get_question_index(focal_question)
172
- ]
173
- prior_question_name = self.question_names[
174
- self._get_question_index(prior_question)
175
- ]
609
+ MemoryManagement(self)._set_memory_plan(prior_questions_func)
610
+
611
+ def add_targeted_memory(
612
+ self,
613
+ focal_question: Union[QuestionBase, str],
614
+ prior_question: Union[QuestionBase, str],
615
+ ) -> Survey:
616
+ """Add instructions to a survey than when answering focal_question.
617
+
618
+ :param focal_question: The question that the agent is answering.
619
+ :param prior_question: The question that the agent should remember when answering the focal question.
176
620
 
177
- self.memory_plan.add_single_memory(
178
- focal_question=focal_question_name,
179
- prior_question=prior_question_name,
621
+ Here we add instructions to a survey than when answering q2 they should remember q1:
622
+
623
+ >>> s = Survey.example().add_targeted_memory("q2", "q0")
624
+ >>> s.memory_plan
625
+ {'q2': Memory(prior_questions=['q0'])}
626
+
627
+ The agent should also remember the answers to prior_questions listed in prior_questions.
628
+ """
629
+ return MemoryManagement(self).add_targeted_memory(
630
+ focal_question, prior_question
180
631
  )
181
632
 
182
633
  def add_memory_collection(
183
634
  self,
184
- focal_question: Union[Question, str],
185
- prior_questions: List[Union[Question, str]],
186
- ):
187
- """This adds instructions to a survey than when answering focal_question,
188
- the agent should also remember the answers to prior_questions listed in prior_questions.
189
- """
190
- focal_question_name = self.question_names[
191
- self._get_question_index(focal_question)
192
- ]
635
+ focal_question: Union[QuestionBase, str],
636
+ prior_questions: List[Union[QuestionBase, str]],
637
+ ) -> Survey:
638
+ """Add prior questions and responses so the agent has them when answering.
193
639
 
194
- prior_question_names = [
195
- self.question_names[self._get_question_index(prior_question)]
196
- for prior_question in prior_questions
197
- ]
640
+ This adds instructions to a survey than when answering focal_question, the agent should also remember the answers to prior_questions listed in prior_questions.
641
+
642
+ :param focal_question: The question that the agent is answering.
643
+ :param prior_questions: The questions that the agent should remember when answering the focal question.
644
+
645
+ Here we have it so that when answering q2, the agent should remember answers to q0 and q1:
198
646
 
199
- self.memory_plan.add_memory_collection(
200
- focal_question=focal_question_name, prior_questions=prior_question_names
647
+ >>> s = Survey.example().add_memory_collection("q2", ["q0", "q1"])
648
+ >>> s.memory_plan
649
+ {'q2': Memory(prior_questions=['q0', 'q1'])}
650
+ """
651
+ return MemoryManagement(self).add_memory_collection(
652
+ focal_question, prior_questions
201
653
  )
202
654
 
203
- def add_stop_rule(self, question: Question, expression: str) -> Survey:
204
- """Adds a rule that stops the survey."""
205
- self.add_rule(question, expression, EndOfSurvey)
206
- return self
655
+ # region: Question groups
656
+ def add_question_group(
657
+ self,
658
+ start_question: Union[QuestionBase, str],
659
+ end_question: Union[QuestionBase, str],
660
+ group_name: str,
661
+ ) -> Survey:
662
+ """Add a group of questions to the survey.
663
+
664
+ :param start_question: The first question in the group.
665
+ :param end_question: The last question in the group.
666
+ :param group_name: The name of the group.
667
+
668
+ Example:
669
+
670
+ >>> s = Survey.example().add_question_group("q0", "q1", "group1")
671
+ >>> s.question_groups
672
+ {'group1': (0, 1)}
673
+
674
+ The name of the group must be a valid identifier:
675
+
676
+ >>> s = Survey.example().add_question_group("q0", "q2", "1group1")
677
+ Traceback (most recent call last):
678
+ ...
679
+ edsl.exceptions.surveys.SurveyCreationError: Group name 1group1 is not a valid identifier.
680
+ ...
681
+ >>> s = Survey.example().add_question_group("q0", "q1", "q0")
682
+ Traceback (most recent call last):
683
+ ...
684
+ edsl.exceptions.surveys.SurveyCreationError: ...
685
+ ...
686
+ >>> s = Survey.example().add_question_group("q1", "q0", "group1")
687
+ Traceback (most recent call last):
688
+ ...
689
+ edsl.exceptions.surveys.SurveyCreationError: ...
690
+ ...
691
+ """
207
692
 
208
- def _get_question_index(self, q):
209
- """Returns the index of the question or EndOfSurvey object. It can handle it if the user
210
- passes in the question name, the question object, or the EndOfSurvey object."""
211
- if q == EndOfSurvey:
212
- return EndOfSurvey
213
- else:
214
- question_name = q if isinstance(q, str) else q.question_name
215
- if question_name not in self.question_name_to_index:
216
- raise ValueError(
217
- f"""Question name {question_name} not found in survey. The current question names are {self.question_name_to_index}."""
693
+ if not group_name.isidentifier():
694
+ raise SurveyCreationError(
695
+ f"Group name {group_name} is not a valid identifier."
696
+ )
697
+
698
+ if group_name in self.question_groups:
699
+ raise SurveyCreationError(
700
+ f"Group name {group_name} already exists in the survey."
701
+ )
702
+
703
+ if group_name in self.question_name_to_index:
704
+ raise SurveyCreationError(
705
+ f"Group name {group_name} already exists as a question name in the survey."
706
+ )
707
+
708
+ start_index = self._get_question_index(start_question)
709
+ end_index = self._get_question_index(end_question)
710
+
711
+ if start_index > end_index:
712
+ raise SurveyCreationError(
713
+ f"Start index {start_index} is greater than end index {end_index}."
714
+ )
715
+
716
+ for existing_group_name, (
717
+ existing_start_index,
718
+ existing_end_index,
719
+ ) in self.question_groups.items():
720
+ if start_index < existing_start_index and end_index > existing_end_index:
721
+ raise SurveyCreationError(
722
+ f"Group {group_name} contains the questions in the new group."
723
+ )
724
+ if start_index > existing_start_index and end_index < existing_end_index:
725
+ raise SurveyCreationError(
726
+ f"Group {group_name} is contained in the new group."
727
+ )
728
+ if start_index < existing_start_index and end_index > existing_start_index:
729
+ raise SurveyCreationError(
730
+ f"Group {group_name} overlaps with the new group."
731
+ )
732
+ if start_index < existing_end_index and end_index > existing_end_index:
733
+ raise SurveyCreationError(
734
+ f"Group {group_name} overlaps with the new group."
218
735
  )
219
- return self.question_name_to_index[question_name]
220
736
 
221
- def add_rule(
222
- self, question: Question, expression: str, next_question: Question
737
+ self.question_groups[group_name] = (start_index, end_index)
738
+ return self
739
+
740
+ # endregion
741
+
742
+ # region: Survey rules
743
+ def show_rules(self) -> None:
744
+ """Print out the rules in the survey.
745
+
746
+ >>> s = Survey.example()
747
+ >>> s.show_rules()
748
+ Dataset([{'current_q': [0, 0, 1, 2]}, {'expression': ['True', "q0 == 'yes'", 'True', 'True']}, {'next_q': [1, 2, 2, 3]}, {'priority': [-1, 0, -1, -1]}, {'before_rule': [False, False, False, False]}])
749
+ """
750
+ return self.rule_collection.show_rules()
751
+
752
+ def add_stop_rule(
753
+ self, question: Union[QuestionBase, str], expression: str
223
754
  ) -> Survey:
755
+ """Add a rule that stops the survey.
756
+ The rule is evaluated *after* the question is answered. If the rule is true, the survey ends.
757
+
758
+ :param question: The question to add the stop rule to.
759
+ :param expression: The expression to evaluate.
760
+
761
+ If this rule is true, the survey ends.
762
+
763
+ Here, answering "yes" to q0 ends the survey:
764
+
765
+ >>> s = Survey.example().add_stop_rule("q0", "q0 == 'yes'")
766
+ >>> s.next_question("q0", {"q0": "yes"})
767
+ EndOfSurvey
768
+
769
+ By comparison, answering "no" to q0 does not end the survey:
770
+
771
+ >>> s.next_question("q0", {"q0": "no"}).question_name
772
+ 'q1'
773
+
774
+ >>> s.add_stop_rule("q0", "q1 <> 'yes'")
775
+ Traceback (most recent call last):
776
+ ...
777
+ edsl.exceptions.surveys.SurveyCreationError: The expression contains '<>', which is not allowed. You probably mean '!='.
778
+ ...
224
779
  """
225
- Adds a rule to a Question of the Survey with the appropriate priority.
226
- - The last rule added for the question will have the highest priority.
227
- - If there are no rules, the rule added gets priority -1.
780
+ return RuleManager(self).add_stop_rule(question, expression)
781
+
782
+ def clear_non_default_rules(self) -> Survey:
783
+ """Remove all non-default rules from the survey.
784
+
785
+ >>> Survey.example().show_rules()
786
+ Dataset([{'current_q': [0, 0, 1, 2]}, {'expression': ['True', "q0 == 'yes'", 'True', 'True']}, {'next_q': [1, 2, 2, 3]}, {'priority': [-1, 0, -1, -1]}, {'before_rule': [False, False, False, False]}])
787
+ >>> Survey.example().clear_non_default_rules().show_rules()
788
+ Dataset([{'current_q': [0, 1, 2]}, {'expression': ['True', 'True', 'True']}, {'next_q': [1, 2, 3]}, {'priority': [-1, -1, -1]}, {'before_rule': [False, False, False]}])
228
789
  """
790
+ s = Survey()
791
+ for question in self.questions:
792
+ s.add_question(question)
793
+ return s
229
794
 
795
+ def add_skip_rule(
796
+ self, question: Union[QuestionBase, str], expression: str
797
+ ) -> Survey:
798
+ """
799
+ Adds a per-question skip rule to the survey.
800
+
801
+ :param question: The question to add the skip rule to.
802
+ :param expression: The expression to evaluate.
803
+
804
+ This adds a rule that skips 'q0' always, before the question is answered:
805
+
806
+ >>> from edsl import QuestionFreeText
807
+ >>> q0 = QuestionFreeText.example()
808
+ >>> q0.question_name = "q0"
809
+ >>> q1 = QuestionFreeText.example()
810
+ >>> q1.question_name = "q1"
811
+ >>> s = Survey([q0, q1]).add_skip_rule("q0", "True")
812
+ >>> s.next_question("q0", {}).question_name
813
+ 'q1'
814
+
815
+ Note that this is different from a rule that jumps to some other question *after* the question is answered.
816
+
817
+ """
230
818
  question_index = self._get_question_index(question)
231
- next_question_index = self._get_question_index(next_question)
232
-
233
- def get_new_rule_priority(question_index):
234
- current_priorities = [
235
- rule.priority
236
- for rule in self.rule_collection.applicable_rules(question_index)
237
- ]
238
- max_priority = max(current_priorities)
239
- # newer rules take priority over older rules
240
- new_priority = (
241
- RulePriority.DEFAULT.value
242
- if len(current_priorities) == 0
243
- else max_priority + 1
244
- )
245
- return new_priority
246
-
247
- self.rule_collection.add_rule(
248
- Rule(
249
- current_q=question_index,
250
- expression=expression,
251
- next_q=next_question_index,
252
- question_name_to_index=self.question_name_to_index,
253
- priority=get_new_rule_priority(question_index),
254
- )
819
+ return RuleManager(self).add_rule(
820
+ question, expression, question_index + 1, before_rule=True
255
821
  )
256
822
 
257
- return self
823
+ def add_rule(
824
+ self,
825
+ question: Union[QuestionBase, str],
826
+ expression: str,
827
+ next_question: Union[QuestionBase, int],
828
+ before_rule: bool = False,
829
+ ) -> Survey:
830
+ """
831
+ Add a rule to a Question of the Survey.
258
832
 
259
- ###################
260
- # FORWARD METHODS
261
- ###################
262
- def by(self, *args: Union[Agent, Scenario, LanguageModel]) -> Jobs:
263
- """Adds Agents, Scenarios, and LanguageModels to a survey and returns a runnable Jobs object"""
833
+ :param question: The question to add the rule to.
834
+ :param expression: The expression to evaluate.
835
+ :param next_question: The next question to go to if the rule is true.
836
+ :param before_rule: Whether the rule is evaluated before the question is answered.
837
+
838
+ This adds a rule that if the answer to q0 is 'yes', the next question is q2 (as opposed to q1)
839
+
840
+ >>> s = Survey.example().add_rule("q0", "{{ q0 }} == 'yes'", "q2")
841
+ >>> s.next_question("q0", {"q0": "yes"}).question_name
842
+ 'q2'
843
+
844
+ """
845
+ return RuleManager(self).add_rule(
846
+ question, expression, next_question, before_rule=before_rule
847
+ )
848
+
849
+ # endregion
850
+
851
+ # region: Forward methods
852
+ def by(self, *args: Union["Agent", "Scenario", "LanguageModel"]) -> "Jobs":
853
+ """Add Agents, Scenarios, and LanguageModels to a survey and returns a runnable Jobs object.
854
+
855
+ :param args: The Agents, Scenarios, and LanguageModels to add to the survey.
856
+
857
+ This takes the survey and adds an Agent and a Scenario via 'by' which converts to a Jobs object:
858
+
859
+ >>> s = Survey.example(); from edsl.agents import Agent; from edsl import Scenario
860
+ >>> s.by(Agent.example()).by(Scenario.example())
861
+ Jobs(...)
862
+ """
264
863
  from edsl.jobs.Jobs import Jobs
265
864
 
266
- job = Jobs(survey=self)
267
- return job.by(*args)
865
+ return Jobs(survey=self).by(*args)
268
866
 
269
- def run(self, *args, **kwargs) -> Jobs:
270
- "Turns the survey into a Job and runs it"
867
+ def to_jobs(self):
868
+ """Convert the survey to a Jobs object.
869
+ >>> s = Survey.example()
870
+ >>> s.to_jobs()
871
+ Jobs(...)
872
+ """
873
+ from edsl.jobs.Jobs import Jobs
874
+
875
+ return Jobs(survey=self)
876
+
877
+ def show_prompts(self):
878
+ """Show the prompts for the survey."""
879
+ return self.to_jobs().show_prompts()
880
+
881
+ # endregion
882
+
883
+ # region: Running the survey
884
+
885
+ def __call__(
886
+ self,
887
+ model=None,
888
+ agent=None,
889
+ cache=None,
890
+ verbose=False,
891
+ disable_remote_cache: bool = False,
892
+ disable_remote_inference: bool = False,
893
+ **kwargs,
894
+ ):
895
+ """Run the survey with default model, taking the required survey as arguments.
896
+
897
+ >>> from edsl.questions import QuestionFunctional
898
+ >>> def f(scenario, agent_traits): return "yes" if scenario["period"] == "morning" else "no"
899
+ >>> q = QuestionFunctional(question_name = "q0", func = f)
900
+ >>> s = Survey([q])
901
+ >>> s(period = "morning", cache = False, disable_remote_cache = True, disable_remote_inference = True).select("answer.q0").first()
902
+ 'yes'
903
+ >>> s(period = "evening", cache = False, disable_remote_cache = True, disable_remote_inference = True).select("answer.q0").first()
904
+ 'no'
905
+ """
906
+
907
+ return self.get_job(model, agent, **kwargs).run(
908
+ cache=cache,
909
+ verbose=verbose,
910
+ disable_remote_cache=disable_remote_cache,
911
+ disable_remote_inference=disable_remote_inference,
912
+ )
913
+
914
+ async def run_async(
915
+ self,
916
+ model: Optional["LanguageModel"] = None,
917
+ agent: Optional["Agent"] = None,
918
+ cache: Optional["Cache"] = None,
919
+ disable_remote_inference: bool = False,
920
+ disable_remote_cache: bool = False,
921
+ **kwargs,
922
+ ):
923
+ """Run the survey with default model, taking the required survey as arguments.
924
+
925
+ >>> import asyncio
926
+ >>> from edsl.questions import QuestionFunctional
927
+ >>> def f(scenario, agent_traits): return "yes" if scenario["period"] == "morning" else "no"
928
+ >>> q = QuestionFunctional(question_name = "q0", func = f)
929
+ >>> s = Survey([q])
930
+ >>> async def test_run_async(): result = await s.run_async(period="morning", disable_remote_inference = True, disable_remote_cache=True); print(result.select("answer.q0").first())
931
+ >>> asyncio.run(test_run_async())
932
+ yes
933
+ >>> import asyncio
934
+ >>> from edsl.questions import QuestionFunctional
935
+ >>> def f(scenario, agent_traits): return "yes" if scenario["period"] == "morning" else "no"
936
+ >>> q = QuestionFunctional(question_name = "q0", func = f)
937
+ >>> s = Survey([q])
938
+ >>> async def test_run_async(): result = await s.run_async(period="evening", disable_remote_inference = True, disable_remote_cache = True); print(result.select("answer.q0").first())
939
+ >>> results = asyncio.run(test_run_async())
940
+ no
941
+ """
942
+ # TODO: temp fix by creating a cache
943
+ if cache is None:
944
+ from edsl.data import Cache
945
+ c = Cache()
946
+ else:
947
+ c = cache
948
+
949
+
950
+
951
+ jobs: "Jobs" = self.get_job(model=model, agent=agent, **kwargs).using(c)
952
+ return await jobs.run_async(
953
+ disable_remote_inference=disable_remote_inference,
954
+ disable_remote_cache=disable_remote_cache,
955
+ )
956
+
957
+ def run(self, *args, **kwargs) -> "Results":
958
+ """Turn the survey into a Job and runs it.
959
+
960
+ >>> from edsl import QuestionFreeText
961
+ >>> s = Survey([QuestionFreeText.example()])
962
+ >>> from edsl.language_models import LanguageModel
963
+ >>> m = LanguageModel.example(test_model = True, canned_response = "Great!")
964
+ >>> results = s.by(m).run(cache = False, disable_remote_cache = True, disable_remote_inference = True)
965
+ >>> results.select('answer.*')
966
+ Dataset([{'answer.how_are_you': ['Great!']}])
967
+ """
271
968
  from edsl.jobs.Jobs import Jobs
272
969
 
273
970
  return Jobs(survey=self).run(*args, **kwargs)
274
971
 
275
- ########################
276
- ## Survey-Taking Methods
277
- ########################
972
+ def using(self, obj: Union["Cache", "KeyLookup", "BucketCollection"]) -> "Jobs":
973
+ """Turn the survey into a Job and appends the arguments to the Job."""
974
+ from edsl.jobs.Jobs import Jobs
975
+
976
+ return Jobs(survey=self).using(obj)
278
977
 
279
- def first_question(self) -> Question:
280
- "Returns the first question in the survey"
281
- return self.questions[0]
978
+ def duplicate(self):
979
+ """Duplicate the survey.
282
980
 
981
+ >>> s = Survey.example()
982
+ >>> s2 = s.duplicate()
983
+ >>> s == s2
984
+ True
985
+ >>> s is s2
986
+ False
987
+
988
+ """
989
+ return Survey.from_dict(self.to_dict())
990
+
991
+ # region: Survey flow
283
992
  def next_question(
284
- self, current_question: "Question", answers: dict
285
- ) -> Union[Question, EndOfSurvey.__class__]:
993
+ self,
994
+ current_question: Optional[Union[str, QuestionBase]] = None,
995
+ answers: Optional[dict] = None,
996
+ ) -> Union[QuestionBase, EndOfSurvey.__class__]:
286
997
  """
287
- Returns the next question in a survey.
998
+ Return the next question in a survey.
999
+
1000
+ :param current_question: The current question in the survey.
1001
+ :param answers: The answers for the survey so far
1002
+
288
1003
  - If called with no arguments, it returns the first question in the survey.
289
1004
  - If no answers are provided for a question with a rule, the next question is returned. If answers are provided, the next question is determined by the rules and the answers.
290
1005
  - If the next question is the last question in the survey, an EndOfSurvey object is returned.
1006
+
1007
+ >>> s = Survey.example()
1008
+ >>> s.next_question("q0", {"q0": "yes"}).question_name
1009
+ 'q2'
1010
+ >>> s.next_question("q0", {"q0": "no"}).question_name
1011
+ 'q1'
1012
+
291
1013
  """
1014
+ if current_question is None:
1015
+ return self.questions[0]
1016
+
292
1017
  if isinstance(current_question, str):
293
- print(
294
- "WARNING: current_question by string is deprecated. Please use a Question object."
295
- )
296
- current_question = self.get_question(current_question)
1018
+ current_question = self._get_question_by_name(current_question)
297
1019
 
298
1020
  question_index = self.question_name_to_index[current_question.question_name]
299
1021
  next_question_object = self.rule_collection.next_question(
@@ -311,149 +1033,163 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
311
1033
  else:
312
1034
  return self.questions[next_question_object.next_q]
313
1035
 
314
- def gen_path_through_survey(self) -> Generator[Question, dict, None]:
1036
+ def gen_path_through_survey(self) -> Generator[QuestionBase, dict, None]:
315
1037
  """
316
- Generates a coroutine that can be used to conduct an Interview.
317
- - The coroutine is a generator that yields a question and receives answers.
318
- - The coroutine starts with the first question in the survey.
319
- - The coroutine ends when an EndOfSurvey object is returned.
1038
+ Generate a coroutine that can be used to conduct an Interview.
1039
+
1040
+ The coroutine is a generator that yields a question and receives answers.
1041
+ It starts with the first question in the survey.
1042
+ The coroutine ends when an EndOfSurvey object is returned.
1043
+
1044
+ For the example survey, this is the rule table:
1045
+
1046
+ >>> s = Survey.example()
1047
+ >>> s.show_rules()
1048
+ Dataset([{'current_q': [0, 0, 1, 2]}, {'expression': ['True', "q0 == 'yes'", 'True', 'True']}, {'next_q': [1, 2, 2, 3]}, {'priority': [-1, 0, -1, -1]}, {'before_rule': [False, False, False, False]}])
1049
+
1050
+ Note that q0 has a rule that if the answer is 'yes', the next question is q2. If the answer is 'no', the next question is q1.
1051
+
1052
+ Here is the path through the survey if the answer to q0 is 'yes':
1053
+
1054
+ >>> i = s.gen_path_through_survey()
1055
+ >>> next(i)
1056
+ Question('multiple_choice', question_name = \"""q0\""", question_text = \"""Do you like school?\""", question_options = ['yes', 'no'])
1057
+ >>> i.send({"q0": "yes"})
1058
+ Question('multiple_choice', question_name = \"""q2\""", question_text = \"""Why?\""", question_options = ['**lack*** of killer bees in cafeteria', 'other'])
1059
+
1060
+ And here is the path through the survey if the answer to q0 is 'no':
1061
+
1062
+ >>> i2 = s.gen_path_through_survey()
1063
+ >>> next(i2)
1064
+ Question('multiple_choice', question_name = \"""q0\""", question_text = \"""Do you like school?\""", question_options = ['yes', 'no'])
1065
+ >>> i2.send({"q0": "no"})
1066
+ Question('multiple_choice', question_name = \"""q1\""", question_text = \"""Why not?\""", question_options = ['killer bees in cafeteria', 'other'])
320
1067
 
321
- E.g., in Interview.py
322
1068
 
323
- path_through_survey = self.survey.gen_path_through_survey()
324
- question = path_through_survey.send({question.question_name: answer})
325
1069
  """
326
- question = self.first_question()
1070
+ self.answers = {}
1071
+ question = self._questions[0]
1072
+ # should the first question be skipped?
1073
+ if self.rule_collection.skip_question_before_running(0, self.answers):
1074
+ question = self.next_question(question, self.answers)
1075
+
327
1076
  while not question == EndOfSurvey:
328
- self.answers = yield question
1077
+ answer = yield question
1078
+ self.answers.update(answer)
1079
+ # print(f"Answers: {self.answers}")
1080
+ ## TODO: This should also include survey and agent attributes
329
1081
  question = self.next_question(question, self.answers)
330
1082
 
331
- @property
332
- def scenario_attributes(self) -> list[str]:
333
- """Returns a list of attributes that admissible Scenarios should have"""
334
- temp = []
335
- for question in self.questions:
336
- question_text = question.question_text
337
- # extract the contents of all {{ }} in the question text using regex
338
- matches = re.findall(r"\{\{(.+?)\}\}", question_text)
339
- # remove whitespace
340
- matches = [match.strip() for match in matches]
341
- # add them to the temp list
342
- temp.extend(matches)
343
- return temp
1083
+ # endregion
1084
+
1085
+ def dag(self, textify: bool = False) -> DAG:
1086
+ """Return the DAG of the survey, which reflects both skip-logic and memory.
1087
+
1088
+ :param textify: Whether to return the DAG with question names instead of indices.
344
1089
 
345
- def textify(self, index_dag: DAG) -> DAG:
346
- """Converts the DAG of question indices to a DAG of question names
347
1090
  >>> s = Survey.example()
348
1091
  >>> d = s.dag()
349
1092
  >>> d
350
1093
  {1: {0}, 2: {0}}
351
- >>> s.textify(d)
352
- {'q1': {'q0'}, 'q2': {'q0'}}
353
- """
354
-
355
- def get_name(index):
356
- return self.questions[index].question_name
357
-
358
- try:
359
- text_dag = {}
360
- for child_index, parent_indices in index_dag.items():
361
- parent_names = {get_name(index) for index in parent_indices}
362
- child_name = get_name(child_index)
363
- text_dag[child_name] = parent_names
364
- return text_dag
365
- except IndexError:
366
- breakpoint()
367
-
368
- def dag(self, textify=False) -> DAG:
369
- """Returns the DAG of the survey, which reflects both skip-logic and memory."""
370
- memory_dag = self.memory_plan.dag
371
- rule_dag = self.rule_collection.dag
372
- if textify:
373
- memory_dag = DAG(self.textify(memory_dag))
374
- rule_dag = DAG(self.textify(rule_dag))
375
- return memory_dag + rule_dag
1094
+
1095
+ """
1096
+ from edsl.surveys.ConstructDAG import ConstructDAG
1097
+
1098
+ return ConstructDAG(self).dag(textify)
376
1099
 
377
1100
  ###################
378
1101
  # DUNDER METHODS
379
1102
  ###################
380
1103
  def __len__(self) -> int:
381
- """Returns the number of questions in the survey"""
1104
+ """Return the number of questions in the survey.
1105
+
1106
+ >>> s = Survey.example()
1107
+ >>> len(s)
1108
+ 3
1109
+ """
382
1110
  return len(self._questions)
383
1111
 
384
- def __getitem__(self, index) -> Question:
385
- """Returns the question object given the question index"""
386
- return self._questions[index]
1112
+ def __getitem__(self, index) -> QuestionBase:
1113
+ """Return the question object given the question index.
387
1114
 
388
- def __eq__(self, other):
389
- """Returns True if the two surveys have the same to_dict."""
390
- if not isinstance(other, Survey):
391
- return False
392
- return self.to_dict() == other.to_dict()
1115
+ :param index: The index of the question to get.
393
1116
 
394
- ###################
395
- # SERIALIZATION METHODS
396
- ###################
397
- def to_dict(self) -> dict[str, Any]:
398
- """Serializes the Survey object to a dictionary."""
399
- return {
400
- "questions": [q.to_dict() for q in self._questions],
401
- "name": self.name,
402
- "memory_plan": self.memory_plan.to_dict(),
403
- "rule_collection": self.rule_collection.to_dict(),
404
- }
1117
+ >>> s = Survey.example()
1118
+ >>> s[0]
1119
+ Question('multiple_choice', question_name = \"""q0\""", question_text = \"""Do you like school?\""", question_options = ['yes', 'no'])
405
1120
 
406
- @classmethod
407
- def from_dict(cls, data: dict) -> Survey:
408
- """Deserializes the dictionary back to a Survey object."""
409
- questions = [Question.from_dict(q_dict) for q_dict in data["questions"]]
410
- memory_plan = MemoryPlan.from_dict(data["memory_plan"])
411
- survey = cls(questions=questions, name=data["name"], memory_plan=memory_plan)
412
- survey.rule_collection = RuleCollection.from_dict(data["rule_collection"])
413
- return survey
1121
+ """
1122
+ if isinstance(index, int):
1123
+ return self._questions[index]
1124
+ elif isinstance(index, str):
1125
+ return getattr(self, index)
1126
+
1127
+ # def _diff(self, other):
1128
+ # """Used for debugging. Print out the differences between two surveys."""
1129
+ # from rich import print
1130
+
1131
+ # for key, value in self.to_dict().items():
1132
+ # if value != other.to_dict()[key]:
1133
+ # print(f"Key: {key}")
1134
+ # print("\n")
1135
+ # print(f"Self: {value}")
1136
+ # print("\n")
1137
+ # print(f"Other: {other.to_dict()[key]}")
1138
+ # print("\n\n")
414
1139
 
415
- ###################
416
- # DISPLAY METHODS
417
- ###################
418
1140
  def __repr__(self) -> str:
419
- """Returns a string representation of the survey"""
420
- questions_string = ", ".join([repr(q) for q in self._questions])
421
- question_names_string = ", ".join([repr(name) for name in self.question_names])
422
- return f"Survey(questions=[{questions_string}], name={repr(self.name)})"
1141
+ """Return a string representation of the survey."""
423
1142
 
424
- def show_rules(self) -> None:
425
- "Prints out the rules in the survey"
426
- self.rule_collection.show_rules()
1143
+ # questions_string = ", ".join([repr(q) for q in self._questions])
1144
+ questions_string = ", ".join([repr(q) for q in self.raw_passed_questions or []])
1145
+ # question_names_string = ", ".join([repr(name) for name in self.question_names])
1146
+ return f"Survey(questions=[{questions_string}], memory_plan={self.memory_plan}, rule_collection={self.rule_collection}, question_groups={self.question_groups}, questions_to_randomize={self.questions_to_randomize})"
427
1147
 
428
- def rich_print(self):
429
- table = Table(show_header=True, header_style="bold magenta")
430
- table.add_column("Questions", style="dim")
1148
+ def _summary(self) -> dict:
1149
+ return {
1150
+ "# questions": len(self),
1151
+ "question_name list": self.question_names,
1152
+ }
431
1153
 
432
- for question in self._questions:
433
- table.add_row(question.rich_print())
1154
+ def tree(self, node_list: Optional[List[str]] = None):
1155
+ return self.to_scenario_list().tree(node_list=node_list)
434
1156
 
435
- return table
1157
+ def table(self, *fields, tablefmt=None) -> Table:
1158
+ return self.to_scenario_list().to_dataset().table(*fields, tablefmt=tablefmt)
436
1159
 
437
- def show_questions(self):
438
- "Prints out the questions in the survey"
439
- for name, question in zip(self.question_names, self._questions):
440
- print(f"Question:{name},{question}")
1160
+ # endregion
441
1161
 
442
1162
  def codebook(self) -> dict[str, str]:
443
- "Creates a codebook for the survey, mapping question names to question text"
1163
+ """Create a codebook for the survey, mapping question names to question text.
1164
+
1165
+ >>> s = Survey.example()
1166
+ >>> s.codebook()
1167
+ {'q0': 'Do you like school?', 'q1': 'Why not?', 'q2': 'Why?'}
1168
+ """
444
1169
  codebook = {}
445
1170
  for question in self._questions:
446
1171
  codebook[question.question_name] = question.question_text
447
1172
  return codebook
448
1173
 
449
1174
  @classmethod
450
- def example(cls) -> Survey:
451
- from edsl.questions.QuestionMultipleChoice import QuestionMultipleChoice
1175
+ def example(
1176
+ cls,
1177
+ params: bool = False,
1178
+ randomize: bool = False,
1179
+ include_instructions=False,
1180
+ custom_instructions: Optional[str] = None,
1181
+ ) -> Survey:
1182
+ """Return an example survey.
452
1183
 
453
- # from edsl.surveys.Survey import Survey
1184
+ >>> s = Survey.example()
1185
+ >>> [q.question_text for q in s.questions]
1186
+ ['Do you like school?', 'Why not?', 'Why?']
1187
+ """
1188
+ from edsl.questions.QuestionMultipleChoice import QuestionMultipleChoice
454
1189
 
1190
+ addition = "" if not randomize else str(uuid4())
455
1191
  q0 = QuestionMultipleChoice(
456
- question_text="Do you like school?",
1192
+ question_text=f"Do you like school?{addition}",
457
1193
  question_options=["yes", "no"],
458
1194
  question_name="q0",
459
1195
  )
@@ -467,33 +1203,71 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
467
1203
  question_options=["**lack*** of killer bees in cafeteria", "other"],
468
1204
  question_name="q2",
469
1205
  )
1206
+ if params:
1207
+ q3 = QuestionMultipleChoice(
1208
+ question_text="To the question '{{ q0.question_text}}', you said '{{ q0.answer }}'. Do you still feel this way?",
1209
+ question_options=["yes", "no"],
1210
+ question_name="q3",
1211
+ )
1212
+ s = cls(questions=[q0, q1, q2, q3])
1213
+ return s
1214
+
1215
+ if include_instructions:
1216
+ from edsl import Instruction
1217
+
1218
+ custom_instructions = (
1219
+ custom_instructions if custom_instructions else "Please pay attention!"
1220
+ )
1221
+
1222
+ i = Instruction(text=custom_instructions, name="attention")
1223
+ s = cls(questions=[i, q0, q1, q2])
1224
+ return s
1225
+
470
1226
  s = cls(questions=[q0, q1, q2])
471
1227
  s = s.add_rule(q0, "q0 == 'yes'", q2)
472
1228
  return s
473
1229
 
1230
+ def get_job(self, model=None, agent=None, **kwargs):
1231
+ if model is None:
1232
+ from edsl.language_models.model import Model
1233
+
1234
+ model = Model()
1235
+
1236
+ from edsl.scenarios.Scenario import Scenario
1237
+
1238
+ s = Scenario(kwargs)
1239
+
1240
+ if not agent:
1241
+ from edsl.agents.Agent import Agent
1242
+
1243
+ agent = Agent()
1244
+
1245
+ return self.by(s).by(agent).by(model)
1246
+
474
1247
 
475
1248
  def main():
1249
+ """Run the example survey."""
1250
+
476
1251
  def example_survey():
477
- from edsl.questions.QuestionMultipleChoice import QuestionMultipleChoice
478
- from edsl.surveys.Survey import Survey
1252
+ """Return an example survey."""
1253
+ from edsl import QuestionMultipleChoice, QuestionList, QuestionNumerical, Survey
479
1254
 
480
1255
  q0 = QuestionMultipleChoice(
481
- question_text="Do you like school?",
482
- question_options=["yes", "no"],
483
1256
  question_name="q0",
1257
+ question_text="What is the capital of France?",
1258
+ question_options=["London", "Paris", "Rome", "Boston", "I don't know"]
484
1259
  )
485
- q1 = QuestionMultipleChoice(
486
- question_text="Why not?",
487
- question_options=["killer bees in cafeteria", "other"],
1260
+ q1 = QuestionList(
488
1261
  question_name="q1",
1262
+ question_text="Name some cities in France.",
1263
+ max_list_items = 5
489
1264
  )
490
- q2 = QuestionMultipleChoice(
491
- question_text="Why?",
492
- question_options=["**lack*** of killer bees in cafeteria", "other"],
1265
+ q2 = QuestionNumerical(
493
1266
  question_name="q2",
1267
+ question_text="What is the population of {{ q0.answer }}?"
494
1268
  )
495
1269
  s = Survey(questions=[q0, q1, q2])
496
- s = s.add_rule(q0, "q0 == 'yes'", q2)
1270
+ s = s.add_rule(q0, "q0 == 'Paris'", q2)
497
1271
  return s
498
1272
 
499
1273
  s = example_survey()
@@ -504,41 +1278,7 @@ def main():
504
1278
 
505
1279
 
506
1280
  if __name__ == "__main__":
507
-
508
- def example_survey():
509
- from edsl.questions.QuestionMultipleChoice import QuestionMultipleChoice
510
- from edsl.surveys.Survey import Survey
511
-
512
- q0 = QuestionMultipleChoice(
513
- question_text="Do you like school?",
514
- question_options=["yes", "no"],
515
- question_name="like_school",
516
- )
517
- q1 = QuestionMultipleChoice(
518
- question_text="Why not?",
519
- question_options=["killer bees in cafeteria", "other"],
520
- question_name="why_not",
521
- )
522
- q2 = QuestionMultipleChoice(
523
- question_text="Why?",
524
- question_options=["**lack*** of killer bees in cafeteria", "other"],
525
- question_name="why",
526
- )
527
- s = Survey(questions=[q0, q1, q2])
528
- s = s.add_rule(q0, "like_school == 'yes'", q2).add_stop_rule(
529
- q1, "why_not == 'other'"
530
- )
531
- return s
532
-
533
- # s = example_survey()
534
- # survey_dict = s.to_dict()
535
- # s2 = Survey.from_dict(survey_dict)
536
- # results = s2.run()
537
- # print(results)
538
-
539
1281
  import doctest
540
1282
 
541
- doctest.testmod()
542
-
543
- s = example_survey()
544
- s.show_flow()
1283
+ # doctest.testmod(optionflags=doctest.ELLIPSIS | doctest.SKIP)
1284
+ doctest.testmod(optionflags=doctest.ELLIPSIS)