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