edsl 0.1.39.dev3__py3-none-any.whl → 0.1.39.dev5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. edsl/Base.py +413 -332
  2. edsl/BaseDiff.py +260 -260
  3. edsl/TemplateLoader.py +24 -24
  4. edsl/__init__.py +57 -49
  5. edsl/__version__.py +1 -1
  6. edsl/agents/Agent.py +1071 -867
  7. edsl/agents/AgentList.py +551 -413
  8. edsl/agents/Invigilator.py +284 -233
  9. edsl/agents/InvigilatorBase.py +257 -270
  10. edsl/agents/PromptConstructor.py +272 -354
  11. edsl/agents/QuestionInstructionPromptBuilder.py +128 -0
  12. edsl/agents/QuestionTemplateReplacementsBuilder.py +137 -0
  13. edsl/agents/__init__.py +2 -3
  14. edsl/agents/descriptors.py +99 -99
  15. edsl/agents/prompt_helpers.py +129 -129
  16. edsl/agents/question_option_processor.py +172 -0
  17. edsl/auto/AutoStudy.py +130 -117
  18. edsl/auto/StageBase.py +243 -230
  19. edsl/auto/StageGenerateSurvey.py +178 -178
  20. edsl/auto/StageLabelQuestions.py +125 -125
  21. edsl/auto/StagePersona.py +61 -61
  22. edsl/auto/StagePersonaDimensionValueRanges.py +88 -88
  23. edsl/auto/StagePersonaDimensionValues.py +74 -74
  24. edsl/auto/StagePersonaDimensions.py +69 -69
  25. edsl/auto/StageQuestions.py +74 -73
  26. edsl/auto/SurveyCreatorPipeline.py +21 -21
  27. edsl/auto/utilities.py +218 -224
  28. edsl/base/Base.py +279 -279
  29. edsl/config.py +177 -157
  30. edsl/conversation/Conversation.py +290 -290
  31. edsl/conversation/car_buying.py +59 -58
  32. edsl/conversation/chips.py +95 -95
  33. edsl/conversation/mug_negotiation.py +81 -81
  34. edsl/conversation/next_speaker_utilities.py +93 -93
  35. edsl/coop/CoopFunctionsMixin.py +15 -0
  36. edsl/coop/ExpectedParrotKeyHandler.py +125 -0
  37. edsl/coop/PriceFetcher.py +54 -54
  38. edsl/coop/__init__.py +2 -2
  39. edsl/coop/coop.py +1106 -1028
  40. edsl/coop/utils.py +131 -131
  41. edsl/data/Cache.py +573 -555
  42. edsl/data/CacheEntry.py +230 -233
  43. edsl/data/CacheHandler.py +168 -149
  44. edsl/data/RemoteCacheSync.py +186 -78
  45. edsl/data/SQLiteDict.py +292 -292
  46. edsl/data/__init__.py +5 -4
  47. edsl/data/orm.py +10 -10
  48. edsl/data_transfer_models.py +74 -73
  49. edsl/enums.py +202 -175
  50. edsl/exceptions/BaseException.py +21 -21
  51. edsl/exceptions/__init__.py +54 -54
  52. edsl/exceptions/agents.py +54 -42
  53. edsl/exceptions/cache.py +5 -5
  54. edsl/exceptions/configuration.py +16 -16
  55. edsl/exceptions/coop.py +10 -10
  56. edsl/exceptions/data.py +14 -14
  57. edsl/exceptions/general.py +34 -34
  58. edsl/exceptions/inference_services.py +5 -0
  59. edsl/exceptions/jobs.py +33 -33
  60. edsl/exceptions/language_models.py +63 -63
  61. edsl/exceptions/prompts.py +15 -15
  62. edsl/exceptions/questions.py +109 -91
  63. edsl/exceptions/results.py +29 -29
  64. edsl/exceptions/scenarios.py +29 -22
  65. edsl/exceptions/surveys.py +37 -37
  66. edsl/inference_services/AnthropicService.py +106 -87
  67. edsl/inference_services/AvailableModelCacheHandler.py +184 -0
  68. edsl/inference_services/AvailableModelFetcher.py +215 -0
  69. edsl/inference_services/AwsBedrock.py +118 -120
  70. edsl/inference_services/AzureAI.py +215 -217
  71. edsl/inference_services/DeepInfraService.py +18 -18
  72. edsl/inference_services/GoogleService.py +143 -148
  73. edsl/inference_services/GroqService.py +20 -20
  74. edsl/inference_services/InferenceServiceABC.py +80 -147
  75. edsl/inference_services/InferenceServicesCollection.py +138 -97
  76. edsl/inference_services/MistralAIService.py +120 -123
  77. edsl/inference_services/OllamaService.py +18 -18
  78. edsl/inference_services/OpenAIService.py +236 -224
  79. edsl/inference_services/PerplexityService.py +160 -163
  80. edsl/inference_services/ServiceAvailability.py +135 -0
  81. edsl/inference_services/TestService.py +90 -89
  82. edsl/inference_services/TogetherAIService.py +172 -170
  83. edsl/inference_services/data_structures.py +134 -0
  84. edsl/inference_services/models_available_cache.py +118 -118
  85. edsl/inference_services/rate_limits_cache.py +25 -25
  86. edsl/inference_services/registry.py +41 -41
  87. edsl/inference_services/write_available.py +10 -10
  88. edsl/jobs/AnswerQuestionFunctionConstructor.py +223 -0
  89. edsl/jobs/Answers.py +43 -56
  90. edsl/jobs/FetchInvigilator.py +47 -0
  91. edsl/jobs/InterviewTaskManager.py +98 -0
  92. edsl/jobs/InterviewsConstructor.py +50 -0
  93. edsl/jobs/Jobs.py +823 -898
  94. edsl/jobs/JobsChecks.py +172 -147
  95. edsl/jobs/JobsComponentConstructor.py +189 -0
  96. edsl/jobs/JobsPrompts.py +270 -268
  97. edsl/jobs/JobsRemoteInferenceHandler.py +311 -239
  98. edsl/jobs/JobsRemoteInferenceLogger.py +239 -0
  99. edsl/jobs/RequestTokenEstimator.py +30 -0
  100. edsl/jobs/__init__.py +1 -1
  101. edsl/jobs/async_interview_runner.py +138 -0
  102. edsl/jobs/buckets/BucketCollection.py +104 -63
  103. edsl/jobs/buckets/ModelBuckets.py +65 -65
  104. edsl/jobs/buckets/TokenBucket.py +283 -251
  105. edsl/jobs/buckets/TokenBucketAPI.py +211 -0
  106. edsl/jobs/buckets/TokenBucketClient.py +191 -0
  107. edsl/jobs/check_survey_scenario_compatibility.py +85 -0
  108. edsl/jobs/data_structures.py +120 -0
  109. edsl/jobs/decorators.py +35 -0
  110. edsl/jobs/interviews/Interview.py +396 -661
  111. edsl/jobs/interviews/InterviewExceptionCollection.py +99 -99
  112. edsl/jobs/interviews/InterviewExceptionEntry.py +186 -186
  113. edsl/jobs/interviews/InterviewStatistic.py +63 -63
  114. edsl/jobs/interviews/InterviewStatisticsCollection.py +25 -25
  115. edsl/jobs/interviews/InterviewStatusDictionary.py +78 -78
  116. edsl/jobs/interviews/InterviewStatusLog.py +92 -92
  117. edsl/jobs/interviews/ReportErrors.py +66 -66
  118. edsl/jobs/interviews/interview_status_enum.py +9 -9
  119. edsl/jobs/jobs_status_enums.py +9 -0
  120. edsl/jobs/loggers/HTMLTableJobLogger.py +304 -0
  121. edsl/jobs/results_exceptions_handler.py +98 -0
  122. edsl/jobs/runners/JobsRunnerAsyncio.py +151 -466
  123. edsl/jobs/runners/JobsRunnerStatus.py +297 -330
  124. edsl/jobs/tasks/QuestionTaskCreator.py +244 -242
  125. edsl/jobs/tasks/TaskCreators.py +64 -64
  126. edsl/jobs/tasks/TaskHistory.py +470 -450
  127. edsl/jobs/tasks/TaskStatusLog.py +23 -23
  128. edsl/jobs/tasks/task_status_enum.py +161 -163
  129. edsl/jobs/tokens/InterviewTokenUsage.py +27 -27
  130. edsl/jobs/tokens/TokenUsage.py +34 -34
  131. edsl/language_models/ComputeCost.py +63 -0
  132. edsl/language_models/LanguageModel.py +626 -668
  133. edsl/language_models/ModelList.py +164 -155
  134. edsl/language_models/PriceManager.py +127 -0
  135. edsl/language_models/RawResponseHandler.py +106 -0
  136. edsl/language_models/RegisterLanguageModelsMeta.py +184 -184
  137. edsl/language_models/ServiceDataSources.py +0 -0
  138. edsl/language_models/__init__.py +2 -3
  139. edsl/language_models/fake_openai_call.py +15 -15
  140. edsl/language_models/fake_openai_service.py +61 -61
  141. edsl/language_models/key_management/KeyLookup.py +63 -0
  142. edsl/language_models/key_management/KeyLookupBuilder.py +273 -0
  143. edsl/language_models/key_management/KeyLookupCollection.py +38 -0
  144. edsl/language_models/key_management/__init__.py +0 -0
  145. edsl/language_models/key_management/models.py +131 -0
  146. edsl/language_models/model.py +256 -0
  147. edsl/language_models/repair.py +156 -156
  148. edsl/language_models/utilities.py +65 -64
  149. edsl/notebooks/Notebook.py +263 -258
  150. edsl/notebooks/NotebookToLaTeX.py +142 -0
  151. edsl/notebooks/__init__.py +1 -1
  152. edsl/prompts/Prompt.py +352 -362
  153. edsl/prompts/__init__.py +2 -2
  154. edsl/questions/ExceptionExplainer.py +77 -0
  155. edsl/questions/HTMLQuestion.py +103 -0
  156. edsl/questions/QuestionBase.py +518 -664
  157. edsl/questions/QuestionBasePromptsMixin.py +221 -217
  158. edsl/questions/QuestionBudget.py +227 -227
  159. edsl/questions/QuestionCheckBox.py +359 -359
  160. edsl/questions/QuestionExtract.py +180 -182
  161. edsl/questions/QuestionFreeText.py +113 -114
  162. edsl/questions/QuestionFunctional.py +166 -166
  163. edsl/questions/QuestionList.py +223 -231
  164. edsl/questions/QuestionMatrix.py +265 -0
  165. edsl/questions/QuestionMultipleChoice.py +330 -286
  166. edsl/questions/QuestionNumerical.py +151 -153
  167. edsl/questions/QuestionRank.py +314 -324
  168. edsl/questions/Quick.py +41 -41
  169. edsl/questions/SimpleAskMixin.py +74 -73
  170. edsl/questions/__init__.py +27 -26
  171. edsl/questions/{AnswerValidatorMixin.py → answer_validator_mixin.py} +334 -289
  172. edsl/questions/compose_questions.py +98 -98
  173. edsl/questions/data_structures.py +20 -0
  174. edsl/questions/decorators.py +21 -21
  175. edsl/questions/derived/QuestionLikertFive.py +76 -76
  176. edsl/questions/derived/QuestionLinearScale.py +90 -87
  177. edsl/questions/derived/QuestionTopK.py +93 -93
  178. edsl/questions/derived/QuestionYesNo.py +82 -82
  179. edsl/questions/descriptors.py +427 -413
  180. edsl/questions/loop_processor.py +149 -0
  181. edsl/questions/prompt_templates/question_budget.jinja +13 -13
  182. edsl/questions/prompt_templates/question_checkbox.jinja +32 -32
  183. edsl/questions/prompt_templates/question_extract.jinja +11 -11
  184. edsl/questions/prompt_templates/question_free_text.jinja +3 -3
  185. edsl/questions/prompt_templates/question_linear_scale.jinja +11 -11
  186. edsl/questions/prompt_templates/question_list.jinja +17 -17
  187. edsl/questions/prompt_templates/question_multiple_choice.jinja +33 -33
  188. edsl/questions/prompt_templates/question_numerical.jinja +36 -36
  189. edsl/questions/{QuestionBaseGenMixin.py → question_base_gen_mixin.py} +168 -161
  190. edsl/questions/question_registry.py +177 -177
  191. edsl/questions/{RegisterQuestionsMeta.py → register_questions_meta.py} +71 -71
  192. edsl/questions/{ResponseValidatorABC.py → response_validator_abc.py} +188 -174
  193. edsl/questions/response_validator_factory.py +34 -0
  194. edsl/questions/settings.py +12 -12
  195. edsl/questions/templates/budget/answering_instructions.jinja +7 -7
  196. edsl/questions/templates/budget/question_presentation.jinja +7 -7
  197. edsl/questions/templates/checkbox/answering_instructions.jinja +10 -10
  198. edsl/questions/templates/checkbox/question_presentation.jinja +22 -22
  199. edsl/questions/templates/extract/answering_instructions.jinja +7 -7
  200. edsl/questions/templates/likert_five/answering_instructions.jinja +10 -10
  201. edsl/questions/templates/likert_five/question_presentation.jinja +11 -11
  202. edsl/questions/templates/linear_scale/answering_instructions.jinja +5 -5
  203. edsl/questions/templates/linear_scale/question_presentation.jinja +5 -5
  204. edsl/questions/templates/list/answering_instructions.jinja +3 -3
  205. edsl/questions/templates/list/question_presentation.jinja +5 -5
  206. edsl/questions/templates/matrix/__init__.py +1 -0
  207. edsl/questions/templates/matrix/answering_instructions.jinja +5 -0
  208. edsl/questions/templates/matrix/question_presentation.jinja +20 -0
  209. edsl/questions/templates/multiple_choice/answering_instructions.jinja +9 -9
  210. edsl/questions/templates/multiple_choice/question_presentation.jinja +11 -11
  211. edsl/questions/templates/numerical/answering_instructions.jinja +6 -6
  212. edsl/questions/templates/numerical/question_presentation.jinja +6 -6
  213. edsl/questions/templates/rank/answering_instructions.jinja +11 -11
  214. edsl/questions/templates/rank/question_presentation.jinja +15 -15
  215. edsl/questions/templates/top_k/answering_instructions.jinja +8 -8
  216. edsl/questions/templates/top_k/question_presentation.jinja +22 -22
  217. edsl/questions/templates/yes_no/answering_instructions.jinja +6 -6
  218. edsl/questions/templates/yes_no/question_presentation.jinja +11 -11
  219. edsl/results/CSSParameterizer.py +108 -108
  220. edsl/results/Dataset.py +587 -424
  221. edsl/results/DatasetExportMixin.py +594 -731
  222. edsl/results/DatasetTree.py +295 -275
  223. edsl/results/MarkdownToDocx.py +122 -0
  224. edsl/results/MarkdownToPDF.py +111 -0
  225. edsl/results/Result.py +557 -465
  226. edsl/results/Results.py +1183 -1165
  227. edsl/results/ResultsExportMixin.py +45 -43
  228. edsl/results/ResultsGGMixin.py +121 -121
  229. edsl/results/TableDisplay.py +125 -198
  230. edsl/results/TextEditor.py +50 -0
  231. edsl/results/__init__.py +2 -2
  232. edsl/results/file_exports.py +252 -0
  233. edsl/results/{ResultsFetchMixin.py → results_fetch_mixin.py} +33 -33
  234. edsl/results/{Selector.py → results_selector.py} +145 -135
  235. edsl/results/{ResultsToolsMixin.py → results_tools_mixin.py} +98 -98
  236. edsl/results/smart_objects.py +96 -0
  237. edsl/results/table_data_class.py +12 -0
  238. edsl/results/table_display.css +77 -77
  239. edsl/results/table_renderers.py +118 -0
  240. edsl/results/tree_explore.py +115 -115
  241. edsl/scenarios/ConstructDownloadLink.py +109 -0
  242. edsl/scenarios/DocumentChunker.py +102 -0
  243. edsl/scenarios/DocxScenario.py +16 -0
  244. edsl/scenarios/FileStore.py +511 -632
  245. edsl/scenarios/PdfExtractor.py +40 -0
  246. edsl/scenarios/Scenario.py +498 -601
  247. edsl/scenarios/ScenarioHtmlMixin.py +65 -64
  248. edsl/scenarios/ScenarioList.py +1458 -1287
  249. edsl/scenarios/ScenarioListExportMixin.py +45 -52
  250. edsl/scenarios/ScenarioListPdfMixin.py +239 -261
  251. edsl/scenarios/__init__.py +3 -4
  252. edsl/scenarios/directory_scanner.py +96 -0
  253. edsl/scenarios/file_methods.py +85 -0
  254. edsl/scenarios/handlers/__init__.py +13 -0
  255. edsl/scenarios/handlers/csv.py +38 -0
  256. edsl/scenarios/handlers/docx.py +76 -0
  257. edsl/scenarios/handlers/html.py +37 -0
  258. edsl/scenarios/handlers/json.py +111 -0
  259. edsl/scenarios/handlers/latex.py +5 -0
  260. edsl/scenarios/handlers/md.py +51 -0
  261. edsl/scenarios/handlers/pdf.py +68 -0
  262. edsl/scenarios/handlers/png.py +39 -0
  263. edsl/scenarios/handlers/pptx.py +105 -0
  264. edsl/scenarios/handlers/py.py +294 -0
  265. edsl/scenarios/handlers/sql.py +313 -0
  266. edsl/scenarios/handlers/sqlite.py +149 -0
  267. edsl/scenarios/handlers/txt.py +33 -0
  268. edsl/scenarios/{ScenarioJoin.py → scenario_join.py} +131 -127
  269. edsl/scenarios/scenario_selector.py +156 -0
  270. edsl/shared.py +1 -1
  271. edsl/study/ObjectEntry.py +173 -173
  272. edsl/study/ProofOfWork.py +113 -113
  273. edsl/study/SnapShot.py +80 -80
  274. edsl/study/Study.py +521 -528
  275. edsl/study/__init__.py +4 -4
  276. edsl/surveys/ConstructDAG.py +92 -0
  277. edsl/surveys/DAG.py +148 -148
  278. edsl/surveys/EditSurvey.py +221 -0
  279. edsl/surveys/InstructionHandler.py +100 -0
  280. edsl/surveys/Memory.py +31 -31
  281. edsl/surveys/MemoryManagement.py +72 -0
  282. edsl/surveys/MemoryPlan.py +244 -244
  283. edsl/surveys/Rule.py +327 -326
  284. edsl/surveys/RuleCollection.py +385 -387
  285. edsl/surveys/RuleManager.py +172 -0
  286. edsl/surveys/Simulator.py +75 -0
  287. edsl/surveys/Survey.py +1280 -1801
  288. edsl/surveys/SurveyCSS.py +273 -261
  289. edsl/surveys/SurveyExportMixin.py +259 -259
  290. edsl/surveys/{SurveyFlowVisualizationMixin.py → SurveyFlowVisualization.py} +181 -179
  291. edsl/surveys/SurveyQualtricsImport.py +284 -284
  292. edsl/surveys/SurveyToApp.py +141 -0
  293. edsl/surveys/__init__.py +5 -3
  294. edsl/surveys/base.py +53 -53
  295. edsl/surveys/descriptors.py +60 -56
  296. edsl/surveys/instructions/ChangeInstruction.py +48 -49
  297. edsl/surveys/instructions/Instruction.py +56 -65
  298. edsl/surveys/instructions/InstructionCollection.py +82 -77
  299. edsl/templates/error_reporting/base.html +23 -23
  300. edsl/templates/error_reporting/exceptions_by_model.html +34 -34
  301. edsl/templates/error_reporting/exceptions_by_question_name.html +16 -16
  302. edsl/templates/error_reporting/exceptions_by_type.html +16 -16
  303. edsl/templates/error_reporting/interview_details.html +115 -115
  304. edsl/templates/error_reporting/interviews.html +19 -19
  305. edsl/templates/error_reporting/overview.html +4 -4
  306. edsl/templates/error_reporting/performance_plot.html +1 -1
  307. edsl/templates/error_reporting/report.css +73 -73
  308. edsl/templates/error_reporting/report.html +117 -117
  309. edsl/templates/error_reporting/report.js +25 -25
  310. edsl/tools/__init__.py +1 -1
  311. edsl/tools/clusters.py +192 -192
  312. edsl/tools/embeddings.py +27 -27
  313. edsl/tools/embeddings_plotting.py +118 -118
  314. edsl/tools/plotting.py +112 -112
  315. edsl/tools/summarize.py +18 -18
  316. edsl/utilities/PrettyList.py +56 -0
  317. edsl/utilities/SystemInfo.py +28 -28
  318. edsl/utilities/__init__.py +22 -22
  319. edsl/utilities/ast_utilities.py +25 -25
  320. edsl/utilities/data/Registry.py +6 -6
  321. edsl/utilities/data/__init__.py +1 -1
  322. edsl/utilities/data/scooter_results.json +1 -1
  323. edsl/utilities/decorators.py +77 -77
  324. edsl/utilities/gcp_bucket/cloud_storage.py +96 -96
  325. edsl/utilities/interface.py +627 -627
  326. edsl/utilities/is_notebook.py +18 -0
  327. edsl/utilities/is_valid_variable_name.py +11 -0
  328. edsl/utilities/naming_utilities.py +263 -263
  329. edsl/utilities/remove_edsl_version.py +24 -0
  330. edsl/utilities/repair_functions.py +28 -28
  331. edsl/utilities/restricted_python.py +70 -70
  332. edsl/utilities/utilities.py +436 -424
  333. {edsl-0.1.39.dev3.dist-info → edsl-0.1.39.dev5.dist-info}/LICENSE +21 -21
  334. {edsl-0.1.39.dev3.dist-info → edsl-0.1.39.dev5.dist-info}/METADATA +13 -11
  335. edsl-0.1.39.dev5.dist-info/RECORD +358 -0
  336. {edsl-0.1.39.dev3.dist-info → edsl-0.1.39.dev5.dist-info}/WHEEL +1 -1
  337. edsl/language_models/KeyLookup.py +0 -30
  338. edsl/language_models/registry.py +0 -190
  339. edsl/language_models/unused/ReplicateBase.py +0 -83
  340. edsl/results/ResultsDBMixin.py +0 -238
  341. edsl-0.1.39.dev3.dist-info/RECORD +0 -277
edsl/study/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- from edsl.study.ObjectEntry import ObjectEntry
2
- from edsl.study.ProofOfWork import ProofOfWork
3
- from edsl.study.SnapShot import SnapShot
4
- from edsl.study.Study import Study
1
+ from edsl.study.ObjectEntry import ObjectEntry
2
+ from edsl.study.ProofOfWork import ProofOfWork
3
+ from edsl.study.SnapShot import SnapShot
4
+ from edsl.study.Study import Study
@@ -0,0 +1,92 @@
1
+ from edsl.surveys.base import EndOfSurvey
2
+ from edsl.surveys.DAG import DAG
3
+ from edsl.exceptions.surveys import SurveyError
4
+
5
+
6
+ class ConstructDAG:
7
+ def __init__(self, survey):
8
+ self.survey = survey
9
+ self.questions = survey.questions
10
+
11
+ self.parameters_by_question = self.survey.parameters_by_question
12
+ self.question_name_to_index = self.survey.question_name_to_index
13
+
14
+ def dag(self, textify: bool = False) -> DAG:
15
+ memory_dag = self.survey.memory_plan.dag
16
+ rule_dag = self.survey.rule_collection.dag
17
+ piping_dag = self.piping_dag
18
+ if textify:
19
+ memory_dag = DAG(self.textify(memory_dag))
20
+ rule_dag = DAG(self.textify(rule_dag))
21
+ piping_dag = DAG(self.textify(piping_dag))
22
+ return memory_dag + rule_dag + piping_dag
23
+
24
+ @property
25
+ def piping_dag(self) -> DAG:
26
+ """Figures out the DAG of piping dependencies.
27
+
28
+ >>> from edsl import Survey
29
+ >>> from edsl import QuestionFreeText
30
+ >>> q0 = QuestionFreeText(question_text="Here is a question", question_name="q0")
31
+ >>> q1 = QuestionFreeText(question_text="You previously answered {{ q0 }}---how do you feel now?", question_name="q1")
32
+ >>> s = Survey([q0, q1])
33
+ >>> ConstructDAG(s).piping_dag
34
+ {1: {0}}
35
+ """
36
+ d = {}
37
+ for question_name, depenencies in self.parameters_by_question.items():
38
+ if depenencies:
39
+ question_index = self.question_name_to_index[question_name]
40
+ for dependency in depenencies:
41
+ if dependency not in self.question_name_to_index:
42
+ pass
43
+ else:
44
+ dependency_index = self.question_name_to_index[dependency]
45
+ if question_index not in d:
46
+ d[question_index] = set()
47
+ d[question_index].add(dependency_index)
48
+ return d
49
+
50
+ def textify(self, index_dag: DAG) -> DAG:
51
+ """Convert the DAG of question indices to a DAG of question names.
52
+
53
+ :param index_dag: The DAG of question indices.
54
+
55
+ Example:
56
+
57
+ >>> from edsl import Survey
58
+ >>> s = Survey.example()
59
+ >>> d = s.dag()
60
+ >>> d
61
+ {1: {0}, 2: {0}}
62
+ >>> ConstructDAG(s).textify(d)
63
+ {'q1': {'q0'}, 'q2': {'q0'}}
64
+ """
65
+
66
+ def get_name(index: int):
67
+ """Return the name of the question given the index."""
68
+ if index >= len(self.questions):
69
+ return EndOfSurvey
70
+ try:
71
+ return self.questions[index].question_name
72
+ except IndexError:
73
+ print(
74
+ f"The index is {index} but the length of the questions is {len(self.questions)}"
75
+ )
76
+ raise SurveyError
77
+
78
+ try:
79
+ text_dag = {}
80
+ for child_index, parent_indices in index_dag.items():
81
+ parent_names = {get_name(index) for index in parent_indices}
82
+ child_name = get_name(child_index)
83
+ text_dag[child_name] = parent_names
84
+ return text_dag
85
+ except IndexError:
86
+ raise
87
+
88
+
89
+ if __name__ == "__main__":
90
+ import doctest
91
+
92
+ doctest.testmod()
edsl/surveys/DAG.py CHANGED
@@ -1,148 +1,148 @@
1
- """Directed Acyclic Graph (DAG) class."""
2
-
3
- from collections import UserDict
4
- from graphlib import TopologicalSorter
5
-
6
-
7
- class DAG(UserDict):
8
- """Class for creating a Directed Acyclic Graph (DAG) from a dictionary."""
9
-
10
- def __init__(self, data: dict):
11
- """Initialize the DAG class."""
12
- super().__init__(data)
13
- self.reverse_mapping = self._create_reverse_mapping()
14
- self.validate_no_cycles()
15
-
16
- def _create_reverse_mapping(self):
17
- """
18
- Create a reverse mapping of the DAG, where the keys are the children and the values are the parents.
19
-
20
- Example usage:
21
-
22
- .. code-block:: python
23
-
24
- data = {"a": ["b", "c"], "b": ["d"], "c": [], "d": []}
25
- dag = DAG(data)
26
- dag._create_reverse_mapping()
27
- {'b': {'a'}, 'c': {'a'}, 'd': {'b'}}
28
-
29
- """
30
- rev_map = {}
31
- for key, values in self.items():
32
- for value in values:
33
- rev_map.setdefault(value, set()).add(key)
34
- return rev_map
35
-
36
- def get_all_children(self, key):
37
- """Get all children of a node in the DAG."""
38
- children = set()
39
-
40
- def dfs(node):
41
- for child in self.reverse_mapping.get(node, []):
42
- if child not in children:
43
- children.add(child)
44
- dfs(child)
45
-
46
- dfs(key)
47
- return children
48
-
49
- def topologically_sorted_nodes(self):
50
- """
51
- Return a sequence of the DAG.
52
-
53
- Example usage:
54
-
55
- .. code-block:: python
56
-
57
- data = {"a": ["b", "c"], "b": ["d"], "c": [], "d": []}
58
- dag = DAG(data)
59
- dag.topologically_sorted_nodes() == ['c', 'd', 'b', 'a']
60
- True
61
-
62
- """
63
- return list(TopologicalSorter(self).static_order())
64
-
65
- def __add__(self, other_dag):
66
- """Combine two DAGs."""
67
- d = {}
68
- combined_keys = set(self.keys()).union(set(other_dag.keys()))
69
- for key in combined_keys:
70
- d[key] = self.get(key, set({})).union(other_dag.get(key, set({})))
71
- return DAG(d)
72
- # if textify:
73
- # return DAG(self.textify(d))
74
- # else:
75
- # return DAG(d)
76
-
77
- def remove_node(self, node: int) -> None:
78
- """Remove a node and all its connections from the DAG."""
79
- self.pop(node, None)
80
- for connections in self.values():
81
- connections.discard(node)
82
- # Adjust remaining nodes if necessary
83
- self._adjust_nodes_after_removal(node)
84
-
85
- def _adjust_nodes_after_removal(self, removed_node: int) -> None:
86
- """Adjust node indices after a node is removed."""
87
- new_dag = {}
88
- for node, connections in self.items():
89
- new_node = node if node < removed_node else node - 1
90
- new_connections = {c if c < removed_node else c - 1 for c in connections}
91
- new_dag[new_node] = new_connections
92
- self.clear()
93
- self.update(new_dag)
94
-
95
- @classmethod
96
- def example(cls):
97
- """Return an example of the `DAG`."""
98
- data = {"a": ["b", "c"], "b": ["d"], "c": [], "d": []}
99
- return cls(data)
100
-
101
- def detect_cycles(self):
102
- """
103
- Detect cycles in the DAG using depth-first search.
104
-
105
- :return: A list of cycles if any are found, otherwise an empty list.
106
- """
107
- visited = set()
108
- path = []
109
- cycles = []
110
-
111
- def dfs(node):
112
- if node in path:
113
- cycle = path[path.index(node) :]
114
- cycles.append(cycle + [node])
115
- return
116
-
117
- if node in visited:
118
- return
119
-
120
- visited.add(node)
121
- path.append(node)
122
-
123
- for child in self.get(node, []):
124
- dfs(child)
125
-
126
- path.pop()
127
-
128
- for node in self:
129
- if node not in visited:
130
- dfs(node)
131
-
132
- return cycles
133
-
134
- def validate_no_cycles(self):
135
- """
136
- Validate that the DAG does not contain any cycles.
137
-
138
- :raises ValueError: If cycles are detected in the DAG.
139
- """
140
- cycles = self.detect_cycles()
141
- if cycles:
142
- raise ValueError(f"Cycles detected in the DAG: {cycles}")
143
-
144
-
145
- if __name__ == "__main__":
146
- import doctest
147
-
148
- doctest.testmod()
1
+ """Directed Acyclic Graph (DAG) class."""
2
+
3
+ from collections import UserDict
4
+ from graphlib import TopologicalSorter
5
+
6
+
7
+ class DAG(UserDict):
8
+ """Class for creating a Directed Acyclic Graph (DAG) from a dictionary."""
9
+
10
+ def __init__(self, data: dict):
11
+ """Initialize the DAG class."""
12
+ super().__init__(data)
13
+ self.reverse_mapping = self._create_reverse_mapping()
14
+ self.validate_no_cycles()
15
+
16
+ def _create_reverse_mapping(self):
17
+ """
18
+ Create a reverse mapping of the DAG, where the keys are the children and the values are the parents.
19
+
20
+ Example usage:
21
+
22
+ .. code-block:: python
23
+
24
+ data = {"a": ["b", "c"], "b": ["d"], "c": [], "d": []}
25
+ dag = DAG(data)
26
+ dag._create_reverse_mapping()
27
+ {'b': {'a'}, 'c': {'a'}, 'd': {'b'}}
28
+
29
+ """
30
+ rev_map = {}
31
+ for key, values in self.items():
32
+ for value in values:
33
+ rev_map.setdefault(value, set()).add(key)
34
+ return rev_map
35
+
36
+ def get_all_children(self, key):
37
+ """Get all children of a node in the DAG."""
38
+ children = set()
39
+
40
+ def dfs(node):
41
+ for child in self.reverse_mapping.get(node, []):
42
+ if child not in children:
43
+ children.add(child)
44
+ dfs(child)
45
+
46
+ dfs(key)
47
+ return children
48
+
49
+ def topologically_sorted_nodes(self):
50
+ """
51
+ Return a sequence of the DAG.
52
+
53
+ Example usage:
54
+
55
+ .. code-block:: python
56
+
57
+ data = {"a": ["b", "c"], "b": ["d"], "c": [], "d": []}
58
+ dag = DAG(data)
59
+ dag.topologically_sorted_nodes() == ['c', 'd', 'b', 'a']
60
+ True
61
+
62
+ """
63
+ return list(TopologicalSorter(self).static_order())
64
+
65
+ def __add__(self, other_dag):
66
+ """Combine two DAGs."""
67
+ d = {}
68
+ combined_keys = set(self.keys()).union(set(other_dag.keys()))
69
+ for key in combined_keys:
70
+ d[key] = self.get(key, set({})).union(other_dag.get(key, set({})))
71
+ return DAG(d)
72
+ # if textify:
73
+ # return DAG(self.textify(d))
74
+ # else:
75
+ # return DAG(d)
76
+
77
+ def remove_node(self, node: int) -> None:
78
+ """Remove a node and all its connections from the DAG."""
79
+ self.pop(node, None)
80
+ for connections in self.values():
81
+ connections.discard(node)
82
+ # Adjust remaining nodes if necessary
83
+ self._adjust_nodes_after_removal(node)
84
+
85
+ def _adjust_nodes_after_removal(self, removed_node: int) -> None:
86
+ """Adjust node indices after a node is removed."""
87
+ new_dag = {}
88
+ for node, connections in self.items():
89
+ new_node = node if node < removed_node else node - 1
90
+ new_connections = {c if c < removed_node else c - 1 for c in connections}
91
+ new_dag[new_node] = new_connections
92
+ self.clear()
93
+ self.update(new_dag)
94
+
95
+ @classmethod
96
+ def example(cls):
97
+ """Return an example of the `DAG`."""
98
+ data = {"a": ["b", "c"], "b": ["d"], "c": [], "d": []}
99
+ return cls(data)
100
+
101
+ def detect_cycles(self):
102
+ """
103
+ Detect cycles in the DAG using depth-first search.
104
+
105
+ :return: A list of cycles if any are found, otherwise an empty list.
106
+ """
107
+ visited = set()
108
+ path = []
109
+ cycles = []
110
+
111
+ def dfs(node):
112
+ if node in path:
113
+ cycle = path[path.index(node) :]
114
+ cycles.append(cycle + [node])
115
+ return
116
+
117
+ if node in visited:
118
+ return
119
+
120
+ visited.add(node)
121
+ path.append(node)
122
+
123
+ for child in self.get(node, []):
124
+ dfs(child)
125
+
126
+ path.pop()
127
+
128
+ for node in self:
129
+ if node not in visited:
130
+ dfs(node)
131
+
132
+ return cycles
133
+
134
+ def validate_no_cycles(self):
135
+ """
136
+ Validate that the DAG does not contain any cycles.
137
+
138
+ :raises ValueError: If cycles are detected in the DAG.
139
+ """
140
+ cycles = self.detect_cycles()
141
+ if cycles:
142
+ raise ValueError(f"Cycles detected in the DAG: {cycles}")
143
+
144
+
145
+ if __name__ == "__main__":
146
+ import doctest
147
+
148
+ doctest.testmod()
@@ -0,0 +1,221 @@
1
+ from typing import Union, Optional, TYPE_CHECKING
2
+ from edsl.exceptions.surveys import SurveyError
3
+
4
+ if TYPE_CHECKING:
5
+ from edsl.questions.QuestionBase import QuestionBase
6
+
7
+ from edsl.exceptions.surveys import SurveyError, SurveyCreationError
8
+ from edsl.surveys.Rule import Rule
9
+ from edsl.surveys.base import RulePriority, EndOfSurvey
10
+
11
+
12
+ class EditSurvey:
13
+ def __init__(self, survey):
14
+ self.survey = survey
15
+
16
+ def move_question(self, identifier: Union[str, int], new_index: int) -> "Survey":
17
+ if isinstance(identifier, str):
18
+ if identifier not in self.survey.question_names:
19
+ raise SurveyError(
20
+ f"Question name '{identifier}' does not exist in the survey."
21
+ )
22
+ index = self.survey.question_name_to_index[identifier]
23
+ elif isinstance(identifier, int):
24
+ if identifier < 0 or identifier >= len(self.survey.questions):
25
+ raise SurveyError(f"Index {identifier} is out of range.")
26
+ index = identifier
27
+ else:
28
+ raise SurveyError(
29
+ "Identifier must be either a string (question name) or an integer (question index)."
30
+ )
31
+
32
+ moving_question = self.survey._questions[index]
33
+
34
+ new_survey = self.survey.delete_question(index)
35
+ new_survey.add_question(moving_question, new_index)
36
+ return new_survey
37
+
38
+ def add_question(
39
+ self, question: "QuestionBase", index: Optional[int] = None
40
+ ) -> "Survey":
41
+ if question.question_name in self.survey.question_names:
42
+ raise SurveyCreationError(
43
+ f"""Question name '{question.question_name}' already exists in survey. Existing names are {self.survey.question_names}."""
44
+ )
45
+ if index is None:
46
+ index = len(self.survey.questions)
47
+
48
+ if index > len(self.survey.questions):
49
+ raise SurveyCreationError(
50
+ f"Index {index} is greater than the number of questions in the survey."
51
+ )
52
+ if index < 0:
53
+ raise SurveyCreationError(f"Index {index} is less than 0.")
54
+
55
+ interior_insertion = index != len(self.survey.questions)
56
+
57
+ # index = len(self.survey.questions)
58
+ # TODO: This is a bit ugly because the user
59
+ # doesn't "know" about _questions - it's generated by the
60
+ # descriptor.
61
+ self.survey._questions.insert(index, question)
62
+
63
+ if interior_insertion:
64
+ for question_name, old_index in self.survey._pseudo_indices.items():
65
+ if old_index >= index:
66
+ self.survey._pseudo_indices[question_name] = old_index + 1
67
+
68
+ self.survey._pseudo_indices[question.question_name] = index
69
+
70
+ ## Re-do question_name to index - this is done automatically
71
+ # for question_name, old_index in self.survey.question_name_to_index.items():
72
+ # if old_index >= index:
73
+ # self.survey.question_name_to_index[question_name] = old_index + 1
74
+
75
+ ## Need to re-do the rule collection and the indices of the questions
76
+
77
+ ## If a rule is before the insertion index and next_q is also before the insertion index, no change needed.
78
+ ## If the rule is before the insertion index but next_q is after the insertion index, increment the next_q by 1
79
+ ## If the rule is after the insertion index, increment the current_q by 1 and the next_q by 1
80
+
81
+ # using index + 1 presumes there is a next question
82
+ if interior_insertion:
83
+ for rule in self.survey.rule_collection:
84
+ if rule.current_q >= index:
85
+ rule.current_q += 1
86
+ if rule.next_q >= index:
87
+ rule.next_q += 1
88
+
89
+ # add a new rule
90
+ self.survey.rule_collection.add_rule(
91
+ Rule(
92
+ current_q=index,
93
+ expression="True",
94
+ next_q=index + 1,
95
+ question_name_to_index=self.survey.question_name_to_index,
96
+ priority=RulePriority.DEFAULT.value,
97
+ )
98
+ )
99
+
100
+ # a question might be added before the memory plan is created
101
+ # it's ok because the memory plan will be updated when it is created
102
+ if hasattr(self.survey, "memory_plan"):
103
+ self.survey.memory_plan.add_question(question)
104
+
105
+ return self.survey
106
+
107
+ def delete_question(self, identifier: Union[str, int]) -> "Survey":
108
+ """
109
+ Delete a question from the survey.
110
+
111
+ :param identifier: The name or index of the question to delete.
112
+ :return: The updated Survey object.
113
+
114
+ >>> from edsl import QuestionMultipleChoice, Survey
115
+ >>> q1 = QuestionMultipleChoice(question_text="Q1", question_options=["A", "B"], question_name="q1")
116
+ >>> q2 = QuestionMultipleChoice(question_text="Q2", question_options=["C", "D"], question_name="q2")
117
+ >>> s = Survey().add_question(q1).add_question(q2)
118
+ >>> _ = s.delete_question("q1")
119
+ >>> len(s.questions)
120
+ 1
121
+ >>> _ = s.delete_question(0)
122
+ >>> len(s.questions)
123
+ 0
124
+ """
125
+ if isinstance(identifier, str):
126
+ if identifier not in self.survey.question_names:
127
+ raise SurveyError(
128
+ f"Question name '{identifier}' does not exist in the survey."
129
+ )
130
+ index = self.survey.question_name_to_index[identifier]
131
+ elif isinstance(identifier, int):
132
+ if identifier < 0 or identifier >= len(self.survey.questions):
133
+ raise SurveyError(f"Index {identifier} is out of range.")
134
+ index = identifier
135
+ else:
136
+ raise SurveyError(
137
+ "Identifier must be either a string (question name) or an integer (question index)."
138
+ )
139
+
140
+ # Remove the question
141
+ deleted_question = self.survey._questions.pop(index)
142
+ del self.survey._pseudo_indices[deleted_question.question_name]
143
+
144
+ # Update indices
145
+ for question_name, old_index in self.survey._pseudo_indices.items():
146
+ if old_index > index:
147
+ self.survey._pseudo_indices[question_name] = old_index - 1
148
+
149
+ # Update rules
150
+ from .RuleCollection import RuleCollection
151
+
152
+ new_rule_collection = RuleCollection()
153
+ for rule in self.survey.rule_collection:
154
+ if rule.current_q == index:
155
+ continue # Remove rules associated with the deleted question
156
+ if rule.current_q > index:
157
+ rule.current_q -= 1
158
+ if rule.next_q > index:
159
+ rule.next_q -= 1
160
+
161
+ if rule.next_q == index:
162
+ if index == len(self.survey.questions):
163
+ rule.next_q = EndOfSurvey
164
+ else:
165
+ rule.next_q = index
166
+
167
+ new_rule_collection.add_rule(rule)
168
+ self.survey.rule_collection = new_rule_collection
169
+
170
+ # Update memory plan if it exists
171
+ if hasattr(self.survey, "memory_plan"):
172
+ self.survey.memory_plan.remove_question(deleted_question.question_name)
173
+
174
+ return self.survey
175
+
176
+ def add_instruction(
177
+ self, instruction: Union["Instruction", "ChangeInstruction"]
178
+ ) -> "Survey":
179
+ """
180
+ Add an instruction to the survey.
181
+
182
+ :param instruction: The instruction to add to the survey.
183
+
184
+ >>> from edsl import Instruction
185
+ >>> from edsl.surveys.Survey import Survey
186
+ >>> i = Instruction(text="Pay attention to the following questions.", name="intro")
187
+ >>> s = Survey().add_instruction(i)
188
+ >>> s._instruction_names_to_instructions
189
+ {'intro': Instruction(name="intro", text="Pay attention to the following questions.")}
190
+ >>> s._pseudo_indices
191
+ {'intro': -0.5}
192
+ """
193
+ import math
194
+
195
+ if instruction.name in self.survey._instruction_names_to_instructions:
196
+ raise SurveyCreationError(
197
+ f"""Instruction name '{instruction.name}' already exists in survey. Existing names are {self.survey._instruction_names_to_instructions.keys()}."""
198
+ )
199
+ self.survey._instruction_names_to_instructions[instruction.name] = instruction
200
+
201
+ # was the last thing added an instruction or a question?
202
+ if self.survey._pseudo_indices.last_item_was_instruction:
203
+ pseudo_index = (
204
+ self.survey._pseudo_indices.max_pseudo_index
205
+ + (
206
+ math.ceil(self.survey._pseudo_indices.max_pseudo_index)
207
+ - self.survey._pseudo_indices.max_pseudo_index
208
+ )
209
+ / 2
210
+ )
211
+ else:
212
+ pseudo_index = self.survey._pseudo_indices.max_pseudo_index + 1.0 / 2.0
213
+ self.survey._pseudo_indices[instruction.name] = pseudo_index
214
+
215
+ return self.survey
216
+
217
+
218
+ if __name__ == "__main__":
219
+ import doctest
220
+
221
+ doctest.testmod()