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