edsl 0.1.39.dev3__py3-none-any.whl → 0.1.39.dev4__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.
- edsl/Base.py +413 -332
- edsl/BaseDiff.py +260 -260
- edsl/TemplateLoader.py +24 -24
- edsl/__init__.py +57 -49
- edsl/__version__.py +1 -1
- edsl/agents/Agent.py +1071 -867
- edsl/agents/AgentList.py +551 -413
- edsl/agents/Invigilator.py +284 -233
- edsl/agents/InvigilatorBase.py +257 -270
- edsl/agents/PromptConstructor.py +272 -354
- edsl/agents/QuestionInstructionPromptBuilder.py +128 -0
- edsl/agents/QuestionTemplateReplacementsBuilder.py +137 -0
- edsl/agents/__init__.py +2 -3
- edsl/agents/descriptors.py +99 -99
- edsl/agents/prompt_helpers.py +129 -129
- edsl/agents/question_option_processor.py +172 -0
- edsl/auto/AutoStudy.py +130 -117
- edsl/auto/StageBase.py +243 -230
- edsl/auto/StageGenerateSurvey.py +178 -178
- edsl/auto/StageLabelQuestions.py +125 -125
- edsl/auto/StagePersona.py +61 -61
- edsl/auto/StagePersonaDimensionValueRanges.py +88 -88
- edsl/auto/StagePersonaDimensionValues.py +74 -74
- edsl/auto/StagePersonaDimensions.py +69 -69
- edsl/auto/StageQuestions.py +74 -73
- edsl/auto/SurveyCreatorPipeline.py +21 -21
- edsl/auto/utilities.py +218 -224
- edsl/base/Base.py +279 -279
- edsl/config.py +177 -157
- edsl/conversation/Conversation.py +290 -290
- edsl/conversation/car_buying.py +59 -58
- edsl/conversation/chips.py +95 -95
- edsl/conversation/mug_negotiation.py +81 -81
- edsl/conversation/next_speaker_utilities.py +93 -93
- edsl/coop/CoopFunctionsMixin.py +15 -0
- edsl/coop/ExpectedParrotKeyHandler.py +125 -0
- edsl/coop/PriceFetcher.py +54 -54
- edsl/coop/__init__.py +2 -2
- edsl/coop/coop.py +1106 -1028
- edsl/coop/utils.py +131 -131
- edsl/data/Cache.py +573 -555
- edsl/data/CacheEntry.py +230 -233
- edsl/data/CacheHandler.py +168 -149
- edsl/data/RemoteCacheSync.py +186 -78
- edsl/data/SQLiteDict.py +292 -292
- edsl/data/__init__.py +5 -4
- edsl/data/hack.py +10 -0
- edsl/data/orm.py +10 -10
- edsl/data_transfer_models.py +74 -73
- edsl/enums.py +202 -175
- edsl/exceptions/BaseException.py +21 -21
- edsl/exceptions/__init__.py +54 -54
- edsl/exceptions/agents.py +54 -42
- edsl/exceptions/cache.py +5 -5
- edsl/exceptions/configuration.py +16 -16
- edsl/exceptions/coop.py +10 -10
- edsl/exceptions/data.py +14 -14
- edsl/exceptions/general.py +34 -34
- edsl/exceptions/inference_services.py +5 -0
- edsl/exceptions/jobs.py +33 -33
- edsl/exceptions/language_models.py +63 -63
- edsl/exceptions/prompts.py +15 -15
- edsl/exceptions/questions.py +109 -91
- edsl/exceptions/results.py +29 -29
- edsl/exceptions/scenarios.py +29 -22
- edsl/exceptions/surveys.py +37 -37
- edsl/inference_services/AnthropicService.py +106 -87
- edsl/inference_services/AvailableModelCacheHandler.py +184 -0
- edsl/inference_services/AvailableModelFetcher.py +215 -0
- edsl/inference_services/AwsBedrock.py +118 -120
- edsl/inference_services/AzureAI.py +215 -217
- edsl/inference_services/DeepInfraService.py +18 -18
- edsl/inference_services/GoogleService.py +143 -148
- edsl/inference_services/GroqService.py +20 -20
- edsl/inference_services/InferenceServiceABC.py +80 -147
- edsl/inference_services/InferenceServicesCollection.py +138 -97
- edsl/inference_services/MistralAIService.py +120 -123
- edsl/inference_services/OllamaService.py +18 -18
- edsl/inference_services/OpenAIService.py +236 -224
- edsl/inference_services/PerplexityService.py +160 -163
- edsl/inference_services/ServiceAvailability.py +135 -0
- edsl/inference_services/TestService.py +90 -89
- edsl/inference_services/TogetherAIService.py +172 -170
- edsl/inference_services/data_structures.py +134 -0
- edsl/inference_services/models_available_cache.py +118 -118
- edsl/inference_services/rate_limits_cache.py +25 -25
- edsl/inference_services/registry.py +41 -41
- edsl/inference_services/write_available.py +10 -10
- edsl/jobs/AnswerQuestionFunctionConstructor.py +223 -0
- edsl/jobs/Answers.py +43 -56
- edsl/jobs/FetchInvigilator.py +47 -0
- edsl/jobs/InterviewTaskManager.py +98 -0
- edsl/jobs/InterviewsConstructor.py +50 -0
- edsl/jobs/Jobs.py +823 -898
- edsl/jobs/JobsChecks.py +172 -147
- edsl/jobs/JobsComponentConstructor.py +189 -0
- edsl/jobs/JobsPrompts.py +270 -268
- edsl/jobs/JobsRemoteInferenceHandler.py +311 -239
- edsl/jobs/JobsRemoteInferenceLogger.py +239 -0
- edsl/jobs/RequestTokenEstimator.py +30 -0
- edsl/jobs/__init__.py +1 -1
- edsl/jobs/async_interview_runner.py +138 -0
- edsl/jobs/buckets/BucketCollection.py +104 -63
- edsl/jobs/buckets/ModelBuckets.py +65 -65
- edsl/jobs/buckets/TokenBucket.py +283 -251
- edsl/jobs/buckets/TokenBucketAPI.py +211 -0
- edsl/jobs/buckets/TokenBucketClient.py +191 -0
- edsl/jobs/check_survey_scenario_compatibility.py +85 -0
- edsl/jobs/data_structures.py +120 -0
- edsl/jobs/decorators.py +35 -0
- edsl/jobs/interviews/Interview.py +396 -661
- edsl/jobs/interviews/InterviewExceptionCollection.py +99 -99
- edsl/jobs/interviews/InterviewExceptionEntry.py +186 -186
- edsl/jobs/interviews/InterviewStatistic.py +63 -63
- edsl/jobs/interviews/InterviewStatisticsCollection.py +25 -25
- edsl/jobs/interviews/InterviewStatusDictionary.py +78 -78
- edsl/jobs/interviews/InterviewStatusLog.py +92 -92
- edsl/jobs/interviews/ReportErrors.py +66 -66
- edsl/jobs/interviews/interview_status_enum.py +9 -9
- edsl/jobs/jobs_status_enums.py +9 -0
- edsl/jobs/loggers/HTMLTableJobLogger.py +304 -0
- edsl/jobs/results_exceptions_handler.py +98 -0
- edsl/jobs/runners/JobsRunnerAsyncio.py +151 -466
- edsl/jobs/runners/JobsRunnerStatus.py +297 -330
- edsl/jobs/tasks/QuestionTaskCreator.py +244 -242
- edsl/jobs/tasks/TaskCreators.py +64 -64
- edsl/jobs/tasks/TaskHistory.py +470 -450
- edsl/jobs/tasks/TaskStatusLog.py +23 -23
- edsl/jobs/tasks/task_status_enum.py +161 -163
- edsl/jobs/tokens/InterviewTokenUsage.py +27 -27
- edsl/jobs/tokens/TokenUsage.py +34 -34
- edsl/language_models/ComputeCost.py +63 -0
- edsl/language_models/LanguageModel.py +626 -668
- edsl/language_models/ModelList.py +164 -155
- edsl/language_models/PriceManager.py +127 -0
- edsl/language_models/RawResponseHandler.py +106 -0
- edsl/language_models/RegisterLanguageModelsMeta.py +184 -184
- edsl/language_models/ServiceDataSources.py +0 -0
- edsl/language_models/__init__.py +2 -3
- edsl/language_models/fake_openai_call.py +15 -15
- edsl/language_models/fake_openai_service.py +61 -61
- edsl/language_models/key_management/KeyLookup.py +63 -0
- edsl/language_models/key_management/KeyLookupBuilder.py +273 -0
- edsl/language_models/key_management/KeyLookupCollection.py +38 -0
- edsl/language_models/key_management/__init__.py +0 -0
- edsl/language_models/key_management/models.py +131 -0
- edsl/language_models/model.py +256 -0
- edsl/language_models/repair.py +156 -156
- edsl/language_models/utilities.py +65 -64
- edsl/notebooks/Notebook.py +263 -258
- edsl/notebooks/NotebookToLaTeX.py +142 -0
- edsl/notebooks/__init__.py +1 -1
- edsl/prompts/Prompt.py +352 -362
- edsl/prompts/__init__.py +2 -2
- edsl/questions/ExceptionExplainer.py +77 -0
- edsl/questions/HTMLQuestion.py +103 -0
- edsl/questions/QuestionBase.py +518 -664
- edsl/questions/QuestionBasePromptsMixin.py +221 -217
- edsl/questions/QuestionBudget.py +227 -227
- edsl/questions/QuestionCheckBox.py +359 -359
- edsl/questions/QuestionExtract.py +180 -182
- edsl/questions/QuestionFreeText.py +113 -114
- edsl/questions/QuestionFunctional.py +166 -166
- edsl/questions/QuestionList.py +223 -231
- edsl/questions/QuestionMatrix.py +265 -0
- edsl/questions/QuestionMultipleChoice.py +330 -286
- edsl/questions/QuestionNumerical.py +151 -153
- edsl/questions/QuestionRank.py +314 -324
- edsl/questions/Quick.py +41 -41
- edsl/questions/SimpleAskMixin.py +74 -73
- edsl/questions/__init__.py +27 -26
- edsl/questions/{AnswerValidatorMixin.py → answer_validator_mixin.py} +334 -289
- edsl/questions/compose_questions.py +98 -98
- edsl/questions/data_structures.py +20 -0
- edsl/questions/decorators.py +21 -21
- edsl/questions/derived/QuestionLikertFive.py +76 -76
- edsl/questions/derived/QuestionLinearScale.py +90 -87
- edsl/questions/derived/QuestionTopK.py +93 -93
- edsl/questions/derived/QuestionYesNo.py +82 -82
- edsl/questions/descriptors.py +427 -413
- edsl/questions/loop_processor.py +149 -0
- edsl/questions/prompt_templates/question_budget.jinja +13 -13
- edsl/questions/prompt_templates/question_checkbox.jinja +32 -32
- edsl/questions/prompt_templates/question_extract.jinja +11 -11
- edsl/questions/prompt_templates/question_free_text.jinja +3 -3
- edsl/questions/prompt_templates/question_linear_scale.jinja +11 -11
- edsl/questions/prompt_templates/question_list.jinja +17 -17
- edsl/questions/prompt_templates/question_multiple_choice.jinja +33 -33
- edsl/questions/prompt_templates/question_numerical.jinja +36 -36
- edsl/questions/{QuestionBaseGenMixin.py → question_base_gen_mixin.py} +168 -161
- edsl/questions/question_registry.py +177 -177
- edsl/questions/{RegisterQuestionsMeta.py → register_questions_meta.py} +71 -71
- edsl/questions/{ResponseValidatorABC.py → response_validator_abc.py} +188 -174
- edsl/questions/response_validator_factory.py +34 -0
- edsl/questions/settings.py +12 -12
- edsl/questions/templates/budget/answering_instructions.jinja +7 -7
- edsl/questions/templates/budget/question_presentation.jinja +7 -7
- edsl/questions/templates/checkbox/answering_instructions.jinja +10 -10
- edsl/questions/templates/checkbox/question_presentation.jinja +22 -22
- edsl/questions/templates/extract/answering_instructions.jinja +7 -7
- edsl/questions/templates/likert_five/answering_instructions.jinja +10 -10
- edsl/questions/templates/likert_five/question_presentation.jinja +11 -11
- edsl/questions/templates/linear_scale/answering_instructions.jinja +5 -5
- edsl/questions/templates/linear_scale/question_presentation.jinja +5 -5
- edsl/questions/templates/list/answering_instructions.jinja +3 -3
- edsl/questions/templates/list/question_presentation.jinja +5 -5
- edsl/questions/templates/matrix/__init__.py +1 -0
- edsl/questions/templates/matrix/answering_instructions.jinja +5 -0
- edsl/questions/templates/matrix/question_presentation.jinja +20 -0
- edsl/questions/templates/multiple_choice/answering_instructions.jinja +9 -9
- edsl/questions/templates/multiple_choice/question_presentation.jinja +11 -11
- edsl/questions/templates/numerical/answering_instructions.jinja +6 -6
- edsl/questions/templates/numerical/question_presentation.jinja +6 -6
- edsl/questions/templates/rank/answering_instructions.jinja +11 -11
- edsl/questions/templates/rank/question_presentation.jinja +15 -15
- edsl/questions/templates/top_k/answering_instructions.jinja +8 -8
- edsl/questions/templates/top_k/question_presentation.jinja +22 -22
- edsl/questions/templates/yes_no/answering_instructions.jinja +6 -6
- edsl/questions/templates/yes_no/question_presentation.jinja +11 -11
- edsl/results/CSSParameterizer.py +108 -108
- edsl/results/Dataset.py +587 -424
- edsl/results/DatasetExportMixin.py +594 -731
- edsl/results/DatasetTree.py +295 -275
- edsl/results/MarkdownToDocx.py +122 -0
- edsl/results/MarkdownToPDF.py +111 -0
- edsl/results/Result.py +557 -465
- edsl/results/Results.py +1183 -1165
- edsl/results/ResultsExportMixin.py +45 -43
- edsl/results/ResultsGGMixin.py +121 -121
- edsl/results/TableDisplay.py +125 -198
- edsl/results/TextEditor.py +50 -0
- edsl/results/__init__.py +2 -2
- edsl/results/file_exports.py +252 -0
- edsl/results/{ResultsFetchMixin.py → results_fetch_mixin.py} +33 -33
- edsl/results/{Selector.py → results_selector.py} +145 -135
- edsl/results/{ResultsToolsMixin.py → results_tools_mixin.py} +98 -98
- edsl/results/smart_objects.py +96 -0
- edsl/results/table_data_class.py +12 -0
- edsl/results/table_display.css +77 -77
- edsl/results/table_renderers.py +118 -0
- edsl/results/tree_explore.py +115 -115
- edsl/scenarios/ConstructDownloadLink.py +109 -0
- edsl/scenarios/DocumentChunker.py +102 -0
- edsl/scenarios/DocxScenario.py +16 -0
- edsl/scenarios/FileStore.py +511 -632
- edsl/scenarios/PdfExtractor.py +40 -0
- edsl/scenarios/Scenario.py +498 -601
- edsl/scenarios/ScenarioHtmlMixin.py +65 -64
- edsl/scenarios/ScenarioList.py +1458 -1287
- edsl/scenarios/ScenarioListExportMixin.py +45 -52
- edsl/scenarios/ScenarioListPdfMixin.py +239 -261
- edsl/scenarios/__init__.py +3 -4
- edsl/scenarios/directory_scanner.py +96 -0
- edsl/scenarios/file_methods.py +85 -0
- edsl/scenarios/handlers/__init__.py +13 -0
- edsl/scenarios/handlers/csv.py +38 -0
- edsl/scenarios/handlers/docx.py +76 -0
- edsl/scenarios/handlers/html.py +37 -0
- edsl/scenarios/handlers/json.py +111 -0
- edsl/scenarios/handlers/latex.py +5 -0
- edsl/scenarios/handlers/md.py +51 -0
- edsl/scenarios/handlers/pdf.py +68 -0
- edsl/scenarios/handlers/png.py +39 -0
- edsl/scenarios/handlers/pptx.py +105 -0
- edsl/scenarios/handlers/py.py +294 -0
- edsl/scenarios/handlers/sql.py +313 -0
- edsl/scenarios/handlers/sqlite.py +149 -0
- edsl/scenarios/handlers/txt.py +33 -0
- edsl/scenarios/{ScenarioJoin.py → scenario_join.py} +131 -127
- edsl/scenarios/scenario_selector.py +156 -0
- edsl/shared.py +1 -1
- edsl/study/ObjectEntry.py +173 -173
- edsl/study/ProofOfWork.py +113 -113
- edsl/study/SnapShot.py +80 -80
- edsl/study/Study.py +521 -528
- edsl/study/__init__.py +4 -4
- edsl/surveys/ConstructDAG.py +92 -0
- edsl/surveys/DAG.py +148 -148
- edsl/surveys/EditSurvey.py +221 -0
- edsl/surveys/InstructionHandler.py +100 -0
- edsl/surveys/Memory.py +31 -31
- edsl/surveys/MemoryManagement.py +72 -0
- edsl/surveys/MemoryPlan.py +244 -244
- edsl/surveys/Rule.py +327 -326
- edsl/surveys/RuleCollection.py +385 -387
- edsl/surveys/RuleManager.py +172 -0
- edsl/surveys/Simulator.py +75 -0
- edsl/surveys/Survey.py +1280 -1801
- edsl/surveys/SurveyCSS.py +273 -261
- edsl/surveys/SurveyExportMixin.py +259 -259
- edsl/surveys/{SurveyFlowVisualizationMixin.py → SurveyFlowVisualization.py} +181 -179
- edsl/surveys/SurveyQualtricsImport.py +284 -284
- edsl/surveys/SurveyToApp.py +141 -0
- edsl/surveys/__init__.py +5 -3
- edsl/surveys/base.py +53 -53
- edsl/surveys/descriptors.py +60 -56
- edsl/surveys/instructions/ChangeInstruction.py +48 -49
- edsl/surveys/instructions/Instruction.py +56 -65
- edsl/surveys/instructions/InstructionCollection.py +82 -77
- edsl/templates/error_reporting/base.html +23 -23
- edsl/templates/error_reporting/exceptions_by_model.html +34 -34
- edsl/templates/error_reporting/exceptions_by_question_name.html +16 -16
- edsl/templates/error_reporting/exceptions_by_type.html +16 -16
- edsl/templates/error_reporting/interview_details.html +115 -115
- edsl/templates/error_reporting/interviews.html +19 -19
- edsl/templates/error_reporting/overview.html +4 -4
- edsl/templates/error_reporting/performance_plot.html +1 -1
- edsl/templates/error_reporting/report.css +73 -73
- edsl/templates/error_reporting/report.html +117 -117
- edsl/templates/error_reporting/report.js +25 -25
- edsl/test_h +1 -0
- edsl/tools/__init__.py +1 -1
- edsl/tools/clusters.py +192 -192
- edsl/tools/embeddings.py +27 -27
- edsl/tools/embeddings_plotting.py +118 -118
- edsl/tools/plotting.py +112 -112
- edsl/tools/summarize.py +18 -18
- edsl/utilities/PrettyList.py +56 -0
- edsl/utilities/SystemInfo.py +28 -28
- edsl/utilities/__init__.py +22 -22
- edsl/utilities/ast_utilities.py +25 -25
- edsl/utilities/data/Registry.py +6 -6
- edsl/utilities/data/__init__.py +1 -1
- edsl/utilities/data/scooter_results.json +1 -1
- edsl/utilities/decorators.py +77 -77
- edsl/utilities/gcp_bucket/cloud_storage.py +96 -96
- edsl/utilities/gcp_bucket/example.py +50 -0
- edsl/utilities/interface.py +627 -627
- edsl/utilities/is_notebook.py +18 -0
- edsl/utilities/is_valid_variable_name.py +11 -0
- edsl/utilities/naming_utilities.py +263 -263
- edsl/utilities/remove_edsl_version.py +24 -0
- edsl/utilities/repair_functions.py +28 -28
- edsl/utilities/restricted_python.py +70 -70
- edsl/utilities/utilities.py +436 -424
- {edsl-0.1.39.dev3.dist-info → edsl-0.1.39.dev4.dist-info}/LICENSE +21 -21
- {edsl-0.1.39.dev3.dist-info → edsl-0.1.39.dev4.dist-info}/METADATA +13 -11
- edsl-0.1.39.dev4.dist-info/RECORD +361 -0
- edsl/language_models/KeyLookup.py +0 -30
- edsl/language_models/registry.py +0 -190
- edsl/language_models/unused/ReplicateBase.py +0 -83
- edsl/results/ResultsDBMixin.py +0 -238
- edsl-0.1.39.dev3.dist-info/RECORD +0 -277
- {edsl-0.1.39.dev3.dist-info → edsl-0.1.39.dev4.dist-info}/WHEEL +0 -0
edsl/surveys/RuleCollection.py
CHANGED
@@ -1,387 +1,385 @@
|
|
1
|
-
"""A collection of rules for a survey."""
|
2
|
-
|
3
|
-
from typing import List, Union, Any, Optional
|
4
|
-
from collections import defaultdict, UserList
|
5
|
-
|
6
|
-
from edsl.exceptions import (
|
7
|
-
SurveyRuleCannotEvaluateError,
|
8
|
-
SurveyRuleCollectionHasNoRulesAtNodeError,
|
9
|
-
)
|
10
|
-
|
11
|
-
from edsl.surveys.Rule import Rule
|
12
|
-
from edsl.surveys.base import EndOfSurvey
|
13
|
-
from edsl.surveys.DAG import DAG
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
)
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
"""
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
"""
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
>>>
|
78
|
-
>>>
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
rule_collection
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
"
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
rule_collection.keys_between(1,
|
256
|
-
[2, 3]
|
257
|
-
rule_collection.keys_between(1,
|
258
|
-
[2, 3
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
rule_collection =
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
for
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
return
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
doctest.testmod(optionflags=doctest.ELLIPSIS)
|
1
|
+
"""A collection of rules for a survey."""
|
2
|
+
|
3
|
+
from typing import List, Union, Any, Optional
|
4
|
+
from collections import defaultdict, UserList, namedtuple
|
5
|
+
|
6
|
+
from edsl.exceptions.surveys import (
|
7
|
+
SurveyRuleCannotEvaluateError,
|
8
|
+
SurveyRuleCollectionHasNoRulesAtNodeError,
|
9
|
+
)
|
10
|
+
|
11
|
+
from edsl.surveys.Rule import Rule
|
12
|
+
from edsl.surveys.base import EndOfSurvey
|
13
|
+
from edsl.surveys.DAG import DAG
|
14
|
+
|
15
|
+
|
16
|
+
NextQuestion = namedtuple(
|
17
|
+
"NextQuestion", "next_q, num_rules_found, expressions_evaluating_to_true, priority"
|
18
|
+
)
|
19
|
+
|
20
|
+
|
21
|
+
class RuleCollection(UserList):
|
22
|
+
"""A collection of rules for a particular survey."""
|
23
|
+
|
24
|
+
def __init__(self, num_questions: Optional[int] = None, rules: List[Rule] = None):
|
25
|
+
"""Initialize the RuleCollection object.
|
26
|
+
|
27
|
+
:param num_questions: The number of questions in the survey.
|
28
|
+
:param rules: A list of Rule objects.
|
29
|
+
"""
|
30
|
+
super().__init__(rules or [])
|
31
|
+
self.num_questions = num_questions
|
32
|
+
|
33
|
+
def __repr__(self):
|
34
|
+
"""Return a string representation of the RuleCollection object.
|
35
|
+
|
36
|
+
Example usage:
|
37
|
+
|
38
|
+
.. code-block:: python
|
39
|
+
|
40
|
+
rule_collection = RuleCollection.example()
|
41
|
+
_ = eval(repr(rule_collection))
|
42
|
+
|
43
|
+
"""
|
44
|
+
return f"RuleCollection(rules={self.data}, num_questions={self.num_questions})"
|
45
|
+
|
46
|
+
def to_dataset(self):
|
47
|
+
"""Return a Dataset object representation of the RuleCollection object."""
|
48
|
+
from edsl.results.Dataset import Dataset
|
49
|
+
|
50
|
+
keys = ["current_q", "expression", "next_q", "priority", "before_rule"]
|
51
|
+
rule_list = {}
|
52
|
+
for rule in sorted(self, key=lambda r: r.current_q):
|
53
|
+
for k in keys:
|
54
|
+
rule_list.setdefault(k, []).append(getattr(rule, k))
|
55
|
+
|
56
|
+
return Dataset([{k: v} for k, v in rule_list.items()])
|
57
|
+
|
58
|
+
def _repr_html_(self):
|
59
|
+
"""Return an HTML representation of the RuleCollection object."""
|
60
|
+
from edsl.results.Dataset import Dataset
|
61
|
+
|
62
|
+
return self.to_dataset()._repr_html_()
|
63
|
+
|
64
|
+
def to_dict(self, add_edsl_version=True):
|
65
|
+
"""Create a dictionary representation of the RuleCollection object."""
|
66
|
+
return {
|
67
|
+
"rules": [rule.to_dict() for rule in self],
|
68
|
+
"num_questions": self.num_questions,
|
69
|
+
}
|
70
|
+
|
71
|
+
@classmethod
|
72
|
+
def from_dict(cls, rule_collection_dict):
|
73
|
+
"""Create a RuleCollection object from a dictionary.
|
74
|
+
|
75
|
+
>>> rule_collection = RuleCollection.example()
|
76
|
+
>>> rule_collection_dict = rule_collection.to_dict()
|
77
|
+
>>> new_rule_collection = RuleCollection.from_dict(rule_collection_dict)
|
78
|
+
>>> repr(new_rule_collection) == repr(rule_collection)
|
79
|
+
True
|
80
|
+
"""
|
81
|
+
rules = [
|
82
|
+
Rule.from_dict(rule_dict) for rule_dict in rule_collection_dict["rules"]
|
83
|
+
]
|
84
|
+
num_questions = rule_collection_dict["num_questions"]
|
85
|
+
new_rc = cls(rules=rules)
|
86
|
+
new_rc.num_questions = num_questions
|
87
|
+
return new_rc
|
88
|
+
|
89
|
+
def add_rule(self, rule: Rule) -> None:
|
90
|
+
"""Add a rule to a survey.
|
91
|
+
|
92
|
+
>>> rule_collection = RuleCollection()
|
93
|
+
>>> rule_collection.add_rule(Rule(current_q=1, expression="q1 == 'yes'", next_q=3, priority=1, question_name_to_index={'q1': 1, 'q2': 2, 'q3': 3, 'q4': 4}))
|
94
|
+
>>> len(rule_collection)
|
95
|
+
1
|
96
|
+
|
97
|
+
>>> rule_collection = RuleCollection()
|
98
|
+
>>> r = Rule(current_q=1, expression="True", next_q=3, priority=1, question_name_to_index={'q1': 1, 'q2': 2, 'q3': 3, 'q4': 4}, before_rule = True)
|
99
|
+
>>> rule_collection.add_rule(r)
|
100
|
+
>>> rule_collection[0] == r
|
101
|
+
True
|
102
|
+
>>> len(rule_collection.applicable_rules(1, before_rule=True))
|
103
|
+
1
|
104
|
+
>>> len(rule_collection.applicable_rules(1, before_rule=False))
|
105
|
+
0
|
106
|
+
"""
|
107
|
+
self.append(rule)
|
108
|
+
|
109
|
+
def show_rules(self) -> None:
|
110
|
+
"""Print the rules in a table.
|
111
|
+
|
112
|
+
|
113
|
+
.. code-block:: python
|
114
|
+
|
115
|
+
rule_collection = RuleCollection.example()
|
116
|
+
rule_collection.show_rules()
|
117
|
+
┏━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━┓
|
118
|
+
┃ current_q ┃ expression ┃ next_q ┃ priority ┃ before_rule ┃
|
119
|
+
┡━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━┩
|
120
|
+
│ 1 │ q1 == 'yes' │ 3 │ 1 │ False │
|
121
|
+
│ 1 │ q1 == 'no' │ 2 │ 1 │ False │
|
122
|
+
└───────────┴─────────────┴────────┴──────────┴─────────────┘
|
123
|
+
"""
|
124
|
+
return self.to_dataset()
|
125
|
+
|
126
|
+
def skip_question_before_running(self, q_now: int, answers: dict[str, Any]) -> bool:
|
127
|
+
"""Determine if a question should be skipped before running the question.
|
128
|
+
|
129
|
+
:param q_now: The current question index.
|
130
|
+
:param answers: The answers to the survey questions.
|
131
|
+
|
132
|
+
>>> rule_collection = RuleCollection()
|
133
|
+
>>> r = Rule(current_q=1, expression="True", next_q=2, priority=1, question_name_to_index={}, before_rule = True)
|
134
|
+
>>> rule_collection.add_rule(r)
|
135
|
+
>>> rule_collection.skip_question_before_running(1, {})
|
136
|
+
True
|
137
|
+
|
138
|
+
>>> rule_collection = RuleCollection()
|
139
|
+
>>> r = Rule(current_q=1, expression="False", next_q=2, priority=1, question_name_to_index={}, before_rule = True)
|
140
|
+
>>> rule_collection.add_rule(r)
|
141
|
+
>>> rule_collection.skip_question_before_running(1, {})
|
142
|
+
False
|
143
|
+
|
144
|
+
"""
|
145
|
+
for rule in self.applicable_rules(q_now, before_rule=True):
|
146
|
+
if rule.evaluate(answers):
|
147
|
+
return True
|
148
|
+
return False
|
149
|
+
|
150
|
+
def applicable_rules(self, q_now: int, before_rule: bool = False) -> list:
|
151
|
+
"""Show the rules that apply at the current node.
|
152
|
+
|
153
|
+
:param q_now: The current question index.
|
154
|
+
:param before_rule: If True, return rules that are of the type that apply before the question is asked.
|
155
|
+
|
156
|
+
Example usage:
|
157
|
+
|
158
|
+
>>> rule_collection = RuleCollection.example()
|
159
|
+
>>> rule_collection.applicable_rules(1)
|
160
|
+
[Rule(current_q=1, expression="q1 == 'yes'", next_q=3, priority=1, question_name_to_index={'q1': 1, 'q2': 2, 'q3': 3, 'q4': 4}, before_rule=False), Rule(current_q=1, expression="q1 == 'no'", next_q=2, priority=1, question_name_to_index={'q1': 1, 'q2': 2, 'q3': 3, 'q4': 4}, before_rule=False)]
|
161
|
+
|
162
|
+
The default is that the rule is applied after the question is asked.
|
163
|
+
If we want to see the rules that apply before the question is asked, we can set before_rule=True.
|
164
|
+
|
165
|
+
.. code-block:: python
|
166
|
+
|
167
|
+
rule_collection = RuleCollection.example()
|
168
|
+
rule_collection.applicable_rules(1)
|
169
|
+
[Rule(current_q=1, expression="q1 == 'yes'", next_q=3, priority=1, question_name_to_index={'q1': 1, 'q2': 2, 'q3': 3, 'q4': 4}), Rule(current_q=1, expression="q1 == 'no'", next_q=2, priority=1, question_name_to_index={'q1': 1, 'q2': 2, 'q3': 3, 'q4': 4})]
|
170
|
+
|
171
|
+
More than one rule can apply. For example, suppose we are at node 1.
|
172
|
+
We could have three rules:
|
173
|
+
1. "q1 == 'a' ==> 3
|
174
|
+
2. "q1 == 'b' ==> 4
|
175
|
+
3. "q1 == 'c' ==> 5
|
176
|
+
"""
|
177
|
+
return [
|
178
|
+
rule
|
179
|
+
for rule in self
|
180
|
+
if rule.current_q == q_now and rule.before_rule == before_rule
|
181
|
+
]
|
182
|
+
|
183
|
+
def next_question(self, q_now: int, answers: dict[str, Any]) -> NextQuestion:
|
184
|
+
"""Find the next question by index, given the rule collection.
|
185
|
+
|
186
|
+
This rule is applied after the question is answered.
|
187
|
+
|
188
|
+
:param q_now: The current question index.
|
189
|
+
:param answers: The answers to the survey questions so far, including the current question.
|
190
|
+
|
191
|
+
>>> rule_collection = RuleCollection.example()
|
192
|
+
>>> rule_collection.next_question(1, {'q1': 'yes'})
|
193
|
+
NextQuestion(next_q=3, num_rules_found=2, expressions_evaluating_to_true=1, priority=1)
|
194
|
+
|
195
|
+
"""
|
196
|
+
expressions_evaluating_to_true = 0
|
197
|
+
next_q = None
|
198
|
+
highest_priority = -2 # start with -2 to 'pick up' the default rule added
|
199
|
+
num_rules_found = 0
|
200
|
+
|
201
|
+
for rule in self.applicable_rules(q_now, before_rule=False):
|
202
|
+
num_rules_found += 1
|
203
|
+
try:
|
204
|
+
if rule.evaluate(answers): # evaluates to True
|
205
|
+
expressions_evaluating_to_true += 1
|
206
|
+
if rule.priority > highest_priority: # higher priority
|
207
|
+
# we have a new champ!
|
208
|
+
next_q, highest_priority = rule.next_q, rule.priority
|
209
|
+
except SurveyRuleCannotEvaluateError:
|
210
|
+
raise
|
211
|
+
|
212
|
+
if num_rules_found == 0:
|
213
|
+
raise SurveyRuleCollectionHasNoRulesAtNodeError(
|
214
|
+
f"No rules found for question {q_now}"
|
215
|
+
)
|
216
|
+
|
217
|
+
## Now we need to check if the *next question* has any 'before; rules that we should follow
|
218
|
+
for rule in self.applicable_rules(next_q, before_rule=True):
|
219
|
+
if rule.evaluate(answers): # rule evaluates to True
|
220
|
+
return self.next_question(next_q, answers)
|
221
|
+
|
222
|
+
return NextQuestion(
|
223
|
+
next_q, num_rules_found, expressions_evaluating_to_true, highest_priority
|
224
|
+
)
|
225
|
+
|
226
|
+
@property
|
227
|
+
def non_default_rules(self) -> List[Rule]:
|
228
|
+
"""Return all rules that are not the default rule.
|
229
|
+
|
230
|
+
>>> rule_collection = RuleCollection.example()
|
231
|
+
>>> len(rule_collection.non_default_rules)
|
232
|
+
2
|
233
|
+
|
234
|
+
Example usage:
|
235
|
+
|
236
|
+
.. code-block:: python
|
237
|
+
|
238
|
+
rule_collection = RuleCollection.example()
|
239
|
+
len(rule_collection.non_default_rules)
|
240
|
+
2
|
241
|
+
|
242
|
+
"""
|
243
|
+
return [rule for rule in self if rule.priority > -1]
|
244
|
+
|
245
|
+
def keys_between(self, start_q, end_q, right_inclusive=True):
|
246
|
+
"""Return a list of all question indices between start_q and end_q.
|
247
|
+
|
248
|
+
Example usage:
|
249
|
+
|
250
|
+
.. code-block:: python
|
251
|
+
|
252
|
+
rule_collection = RuleCollection(num_questions=5)
|
253
|
+
rule_collection.keys_between(1, 3)
|
254
|
+
[2, 3]
|
255
|
+
rule_collection.keys_between(1, 4)
|
256
|
+
[2, 3, 4]
|
257
|
+
rule_collection.keys_between(1, EndOfSurvey, right_inclusive=False)
|
258
|
+
[2, 3]
|
259
|
+
|
260
|
+
"""
|
261
|
+
# If it's the end of the survey, all questions between the start_q and the end of the survey now depend on the start_q
|
262
|
+
if end_q == EndOfSurvey:
|
263
|
+
if self.num_questions is None:
|
264
|
+
raise ValueError(
|
265
|
+
"Cannot determine DAG when EndOfSurvey and when num_questions is not known."
|
266
|
+
)
|
267
|
+
end_q = self.num_questions - 1
|
268
|
+
|
269
|
+
question_range = list(range(start_q + 1, end_q + int(right_inclusive)))
|
270
|
+
|
271
|
+
return question_range
|
272
|
+
|
273
|
+
@property
|
274
|
+
def dag(self) -> dict:
|
275
|
+
"""
|
276
|
+
Find the DAG of the survey, based on the skip logic.
|
277
|
+
|
278
|
+
Keys are children questions; the list of values are nodes that must be answered first
|
279
|
+
|
280
|
+
Rules are designated at the current question and then direct where
|
281
|
+
control goes next. As such, the destination nodes are the keys
|
282
|
+
and the current nodes are the values. Furthermore, all questions between
|
283
|
+
the current and destination nodes are also included as keys, as they will depend
|
284
|
+
on the answer to the focal node as well.
|
285
|
+
|
286
|
+
For exmaple, if we have a rule that says "if q1 == 'yes', go to q3", then q3 depends on q1, but so does q2.
|
287
|
+
So the DAG would be {3: [1], 2: [1]}.
|
288
|
+
|
289
|
+
Example usage:
|
290
|
+
|
291
|
+
.. code-block:: python
|
292
|
+
|
293
|
+
rule_collection = RuleCollection(num_questions=5)
|
294
|
+
qn2i = {'q1': 1, 'q2': 2, 'q3': 3, 'q4': 4}
|
295
|
+
rule_collection.add_rule(Rule(current_q=1, expression="q1 == 'yes'", next_q=3, priority=1, question_name_to_index = qn2i))
|
296
|
+
rule_collection.add_rule(Rule(current_q=1, expression="q1 == 'no'", next_q=2, priority=1, question_name_to_index = qn2i))
|
297
|
+
rule_collection.dag
|
298
|
+
{2: {1}, 3: {1}}
|
299
|
+
|
300
|
+
"""
|
301
|
+
children_to_parents = defaultdict(set)
|
302
|
+
# We are only interested in non-default rules. Default rules are those
|
303
|
+
# that just go to the next question, so they don't add any dependencies
|
304
|
+
|
305
|
+
## I think for a skip-question, the potenially-skippable question
|
306
|
+
## depends on all the other questions bein answered first.
|
307
|
+
for rule in self.non_default_rules:
|
308
|
+
if not rule.before_rule:
|
309
|
+
# for a regular rule, the next question depends on the current question answer
|
310
|
+
current_q, next_q = rule.current_q, rule.next_q
|
311
|
+
for q in self.keys_between(current_q, next_q):
|
312
|
+
children_to_parents[q].add(current_q)
|
313
|
+
else:
|
314
|
+
# for the 'before rule' skipping depends on all previous answers.
|
315
|
+
focal_q = rule.current_q
|
316
|
+
for q in range(0, focal_q):
|
317
|
+
children_to_parents[focal_q].add(q)
|
318
|
+
|
319
|
+
return DAG(dict(sorted(children_to_parents.items())))
|
320
|
+
|
321
|
+
def detect_cycles(self):
|
322
|
+
"""
|
323
|
+
Detect cycles in the survey rules using depth-first search.
|
324
|
+
|
325
|
+
:return: A list of cycles if any are found, otherwise an empty list.
|
326
|
+
"""
|
327
|
+
dag = self.dag
|
328
|
+
visited = set()
|
329
|
+
path = []
|
330
|
+
cycles = []
|
331
|
+
|
332
|
+
def dfs(node):
|
333
|
+
if node in path:
|
334
|
+
cycle = path[path.index(node) :]
|
335
|
+
cycles.append(cycle + [node])
|
336
|
+
return
|
337
|
+
|
338
|
+
if node in visited:
|
339
|
+
return
|
340
|
+
|
341
|
+
visited.add(node)
|
342
|
+
path.append(node)
|
343
|
+
|
344
|
+
for child in dag.get(node, []):
|
345
|
+
dfs(child)
|
346
|
+
|
347
|
+
path.pop()
|
348
|
+
|
349
|
+
for node in dag:
|
350
|
+
if node not in visited:
|
351
|
+
dfs(node)
|
352
|
+
|
353
|
+
return cycles
|
354
|
+
|
355
|
+
@classmethod
|
356
|
+
def example(cls):
|
357
|
+
"""Create an example RuleCollection object."""
|
358
|
+
qn2i = {"q1": 1, "q2": 2, "q3": 3, "q4": 4}
|
359
|
+
return cls(
|
360
|
+
num_questions=5,
|
361
|
+
rules=[
|
362
|
+
Rule(
|
363
|
+
current_q=1,
|
364
|
+
expression="q1 == 'yes'",
|
365
|
+
next_q=3,
|
366
|
+
priority=1,
|
367
|
+
question_name_to_index=qn2i,
|
368
|
+
),
|
369
|
+
Rule(
|
370
|
+
current_q=1,
|
371
|
+
expression="q1 == 'no'",
|
372
|
+
next_q=2,
|
373
|
+
priority=1,
|
374
|
+
question_name_to_index=qn2i,
|
375
|
+
),
|
376
|
+
],
|
377
|
+
)
|
378
|
+
|
379
|
+
|
380
|
+
if __name__ == "__main__":
|
381
|
+
import doctest
|
382
|
+
|
383
|
+
doctest.testmod(optionflags=doctest.ELLIPSIS)
|
384
|
+
|
385
|
+
print(RuleCollection.example()._repr_html_())
|