edsl 0.1.38.dev4__py3-none-any.whl → 0.1.39__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 +197 -116
- edsl/__init__.py +15 -7
- edsl/__version__.py +1 -1
- edsl/agents/Agent.py +351 -147
- edsl/agents/AgentList.py +211 -73
- edsl/agents/Invigilator.py +101 -50
- edsl/agents/InvigilatorBase.py +62 -70
- edsl/agents/PromptConstructor.py +143 -225
- edsl/agents/QuestionInstructionPromptBuilder.py +128 -0
- edsl/agents/QuestionTemplateReplacementsBuilder.py +137 -0
- edsl/agents/__init__.py +0 -1
- edsl/agents/prompt_helpers.py +3 -3
- edsl/agents/question_option_processor.py +172 -0
- edsl/auto/AutoStudy.py +18 -5
- edsl/auto/StageBase.py +53 -40
- edsl/auto/StageQuestions.py +2 -1
- edsl/auto/utilities.py +0 -6
- edsl/config.py +22 -2
- edsl/conversation/car_buying.py +2 -1
- edsl/coop/CoopFunctionsMixin.py +15 -0
- edsl/coop/ExpectedParrotKeyHandler.py +125 -0
- edsl/coop/PriceFetcher.py +1 -1
- edsl/coop/coop.py +125 -47
- edsl/coop/utils.py +14 -14
- edsl/data/Cache.py +45 -27
- edsl/data/CacheEntry.py +12 -15
- edsl/data/CacheHandler.py +31 -12
- edsl/data/RemoteCacheSync.py +154 -46
- edsl/data/__init__.py +4 -3
- edsl/data_transfer_models.py +2 -1
- edsl/enums.py +27 -0
- edsl/exceptions/__init__.py +50 -50
- edsl/exceptions/agents.py +12 -0
- edsl/exceptions/inference_services.py +5 -0
- edsl/exceptions/questions.py +24 -6
- edsl/exceptions/scenarios.py +7 -0
- edsl/inference_services/AnthropicService.py +38 -19
- edsl/inference_services/AvailableModelCacheHandler.py +184 -0
- edsl/inference_services/AvailableModelFetcher.py +215 -0
- edsl/inference_services/AwsBedrock.py +0 -2
- edsl/inference_services/AzureAI.py +0 -2
- edsl/inference_services/GoogleService.py +7 -12
- edsl/inference_services/InferenceServiceABC.py +18 -85
- edsl/inference_services/InferenceServicesCollection.py +120 -79
- edsl/inference_services/MistralAIService.py +0 -3
- edsl/inference_services/OpenAIService.py +47 -35
- edsl/inference_services/PerplexityService.py +0 -3
- edsl/inference_services/ServiceAvailability.py +135 -0
- edsl/inference_services/TestService.py +11 -10
- edsl/inference_services/TogetherAIService.py +5 -3
- edsl/inference_services/data_structures.py +134 -0
- edsl/jobs/AnswerQuestionFunctionConstructor.py +223 -0
- edsl/jobs/Answers.py +1 -14
- edsl/jobs/FetchInvigilator.py +47 -0
- edsl/jobs/InterviewTaskManager.py +98 -0
- edsl/jobs/InterviewsConstructor.py +50 -0
- edsl/jobs/Jobs.py +356 -431
- edsl/jobs/JobsChecks.py +35 -10
- edsl/jobs/JobsComponentConstructor.py +189 -0
- edsl/jobs/JobsPrompts.py +6 -4
- edsl/jobs/JobsRemoteInferenceHandler.py +205 -133
- 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 +44 -3
- edsl/jobs/buckets/TokenBucket.py +53 -21
- 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 +143 -408
- 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 +88 -403
- edsl/jobs/runners/JobsRunnerStatus.py +133 -165
- edsl/jobs/tasks/QuestionTaskCreator.py +21 -19
- edsl/jobs/tasks/TaskHistory.py +38 -18
- edsl/jobs/tasks/task_status_enum.py +0 -2
- edsl/language_models/ComputeCost.py +63 -0
- edsl/language_models/LanguageModel.py +194 -236
- edsl/language_models/ModelList.py +28 -19
- edsl/language_models/PriceManager.py +127 -0
- edsl/language_models/RawResponseHandler.py +106 -0
- edsl/language_models/ServiceDataSources.py +0 -0
- edsl/language_models/__init__.py +1 -2
- 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 +2 -2
- edsl/language_models/utilities.py +5 -4
- edsl/notebooks/Notebook.py +19 -14
- edsl/notebooks/NotebookToLaTeX.py +142 -0
- edsl/prompts/Prompt.py +29 -39
- edsl/questions/ExceptionExplainer.py +77 -0
- edsl/questions/HTMLQuestion.py +103 -0
- edsl/questions/QuestionBase.py +68 -214
- edsl/questions/QuestionBasePromptsMixin.py +7 -3
- edsl/questions/QuestionBudget.py +1 -1
- edsl/questions/QuestionCheckBox.py +3 -3
- edsl/questions/QuestionExtract.py +5 -7
- edsl/questions/QuestionFreeText.py +2 -3
- edsl/questions/QuestionList.py +10 -18
- edsl/questions/QuestionMatrix.py +265 -0
- edsl/questions/QuestionMultipleChoice.py +67 -23
- edsl/questions/QuestionNumerical.py +2 -4
- edsl/questions/QuestionRank.py +7 -17
- edsl/questions/SimpleAskMixin.py +4 -3
- edsl/questions/__init__.py +2 -1
- edsl/questions/{AnswerValidatorMixin.py → answer_validator_mixin.py} +47 -2
- edsl/questions/data_structures.py +20 -0
- edsl/questions/derived/QuestionLinearScale.py +6 -3
- edsl/questions/derived/QuestionTopK.py +1 -1
- edsl/questions/descriptors.py +17 -3
- edsl/questions/loop_processor.py +149 -0
- edsl/questions/{QuestionBaseGenMixin.py → question_base_gen_mixin.py} +57 -50
- edsl/questions/question_registry.py +1 -1
- edsl/questions/{ResponseValidatorABC.py → response_validator_abc.py} +40 -26
- edsl/questions/response_validator_factory.py +34 -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/results/CSSParameterizer.py +1 -1
- edsl/results/Dataset.py +170 -7
- edsl/results/DatasetExportMixin.py +168 -305
- edsl/results/DatasetTree.py +28 -8
- edsl/results/MarkdownToDocx.py +122 -0
- edsl/results/MarkdownToPDF.py +111 -0
- edsl/results/Result.py +298 -206
- edsl/results/Results.py +149 -131
- edsl/results/ResultsExportMixin.py +2 -0
- edsl/results/TableDisplay.py +98 -171
- edsl/results/TextEditor.py +50 -0
- edsl/results/__init__.py +1 -1
- edsl/results/file_exports.py +252 -0
- edsl/results/{Selector.py → results_selector.py} +23 -13
- edsl/results/smart_objects.py +96 -0
- edsl/results/table_data_class.py +12 -0
- edsl/results/table_renderers.py +118 -0
- edsl/scenarios/ConstructDownloadLink.py +109 -0
- edsl/scenarios/DocumentChunker.py +102 -0
- edsl/scenarios/DocxScenario.py +16 -0
- edsl/scenarios/FileStore.py +150 -239
- edsl/scenarios/PdfExtractor.py +40 -0
- edsl/scenarios/Scenario.py +90 -193
- edsl/scenarios/ScenarioHtmlMixin.py +4 -3
- edsl/scenarios/ScenarioList.py +415 -244
- edsl/scenarios/ScenarioListExportMixin.py +0 -7
- edsl/scenarios/ScenarioListPdfMixin.py +15 -37
- edsl/scenarios/__init__.py +1 -2
- 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/{ScenarioJoin.py → scenario_join.py} +10 -6
- edsl/scenarios/scenario_selector.py +156 -0
- edsl/study/ObjectEntry.py +1 -1
- edsl/study/SnapShot.py +1 -1
- edsl/study/Study.py +5 -12
- edsl/surveys/ConstructDAG.py +92 -0
- edsl/surveys/EditSurvey.py +221 -0
- edsl/surveys/InstructionHandler.py +100 -0
- edsl/surveys/MemoryManagement.py +72 -0
- edsl/surveys/Rule.py +5 -4
- edsl/surveys/RuleCollection.py +25 -27
- edsl/surveys/RuleManager.py +172 -0
- edsl/surveys/Simulator.py +75 -0
- edsl/surveys/Survey.py +270 -791
- edsl/surveys/SurveyCSS.py +20 -8
- edsl/surveys/{SurveyFlowVisualizationMixin.py → SurveyFlowVisualization.py} +11 -9
- edsl/surveys/SurveyToApp.py +141 -0
- edsl/surveys/__init__.py +4 -2
- edsl/surveys/descriptors.py +6 -2
- edsl/surveys/instructions/ChangeInstruction.py +1 -2
- edsl/surveys/instructions/Instruction.py +4 -13
- edsl/surveys/instructions/InstructionCollection.py +11 -6
- edsl/templates/error_reporting/interview_details.html +1 -1
- edsl/templates/error_reporting/report.html +1 -1
- edsl/tools/plotting.py +1 -1
- edsl/utilities/PrettyList.py +56 -0
- edsl/utilities/is_notebook.py +18 -0
- edsl/utilities/is_valid_variable_name.py +11 -0
- edsl/utilities/remove_edsl_version.py +24 -0
- edsl/utilities/utilities.py +35 -23
- {edsl-0.1.38.dev4.dist-info → edsl-0.1.39.dist-info}/METADATA +12 -10
- edsl-0.1.39.dist-info/RECORD +358 -0
- {edsl-0.1.38.dev4.dist-info → edsl-0.1.39.dist-info}/WHEEL +1 -1
- edsl/language_models/KeyLookup.py +0 -30
- edsl/language_models/registry.py +0 -190
- edsl/language_models/unused/ReplicateBase.py +0 -83
- edsl/results/ResultsDBMixin.py +0 -238
- edsl-0.1.38.dev4.dist-info/RECORD +0 -277
- /edsl/questions/{RegisterQuestionsMeta.py → register_questions_meta.py} +0 -0
- /edsl/results/{ResultsFetchMixin.py → results_fetch_mixin.py} +0 -0
- /edsl/results/{ResultsToolsMixin.py → results_tools_mixin.py} +0 -0
- {edsl-0.1.38.dev4.dist-info → edsl-0.1.39.dist-info}/LICENSE +0 -0
edsl/surveys/Survey.py
CHANGED
@@ -2,43 +2,93 @@
|
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
import re
|
5
|
-
import
|
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
|
+
)
|
9
17
|
from uuid import uuid4
|
10
18
|
from edsl.Base import Base
|
11
|
-
from edsl.exceptions import SurveyCreationError, SurveyHasNoRulesError
|
19
|
+
from edsl.exceptions.surveys import SurveyCreationError, SurveyHasNoRulesError
|
12
20
|
from edsl.exceptions.surveys import SurveyError
|
21
|
+
from collections import UserDict
|
13
22
|
|
14
|
-
from edsl.questions.QuestionBase import QuestionBase
|
15
|
-
from edsl.surveys.base import RulePriority, EndOfSurvey
|
16
|
-
from edsl.surveys.DAG import DAG
|
17
|
-
from edsl.surveys.descriptors import QuestionsDescriptor
|
18
|
-
from edsl.surveys.MemoryPlan import MemoryPlan
|
19
|
-
from edsl.surveys.Rule import Rule
|
20
|
-
from edsl.surveys.RuleCollection import RuleCollection
|
21
|
-
from edsl.surveys.SurveyExportMixin import SurveyExportMixin
|
22
|
-
from edsl.surveys.SurveyFlowVisualizationMixin import SurveyFlowVisualizationMixin
|
23
|
-
from edsl.utilities.decorators import add_edsl_version, remove_edsl_version
|
24
23
|
|
25
|
-
|
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())
|
34
|
+
|
35
|
+
@property
|
36
|
+
def last_item_was_instruction(self) -> bool:
|
37
|
+
"""Return whether the last item added to the survey was an instruction.
|
38
|
+
|
39
|
+
This is used to determine the pseudo-index of the next item added to the survey.
|
40
|
+
|
41
|
+
Example:
|
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)
|
52
|
+
|
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
|
61
|
+
|
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
|
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
|
26
73
|
|
27
74
|
from edsl.surveys.instructions.InstructionCollection import InstructionCollection
|
28
75
|
from edsl.surveys.instructions.Instruction import Instruction
|
29
76
|
from edsl.surveys.instructions.ChangeInstruction import ChangeInstruction
|
30
77
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
39
89
|
|
40
90
|
|
41
|
-
class Survey(SurveyExportMixin,
|
91
|
+
class Survey(SurveyExportMixin, Base):
|
42
92
|
"""A collection of questions that supports skip logic."""
|
43
93
|
|
44
94
|
__documentation__ = """https://docs.expectedparrot.com/en/latest/surveys.html"""
|
@@ -61,13 +111,12 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
61
111
|
|
62
112
|
def __init__(
|
63
113
|
self,
|
64
|
-
questions: Optional[
|
65
|
-
|
66
|
-
] = None,
|
67
|
-
|
68
|
-
rule_collection: Optional[RuleCollection] = None,
|
69
|
-
question_groups: Optional[dict[str, tuple[int, int]]] = None,
|
114
|
+
questions: Optional[List["QuestionType"]] = None,
|
115
|
+
memory_plan: Optional["MemoryPlan"] = None,
|
116
|
+
rule_collection: Optional["RuleCollection"] = None,
|
117
|
+
question_groups: Optional["QuestionGroupType"] = None,
|
70
118
|
name: Optional[str] = None,
|
119
|
+
questions_to_randomize: Optional[List[str]] = None,
|
71
120
|
):
|
72
121
|
"""Create a new survey.
|
73
122
|
|
@@ -89,11 +138,7 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
89
138
|
|
90
139
|
self.raw_passed_questions = questions
|
91
140
|
|
92
|
-
(
|
93
|
-
true_questions,
|
94
|
-
instruction_names_to_instructions,
|
95
|
-
self.pseudo_indices,
|
96
|
-
) = self._separate_questions_and_instructions(questions or [])
|
141
|
+
true_questions = self._process_raw_questions(self.raw_passed_questions)
|
97
142
|
|
98
143
|
self.rule_collection = RuleCollection(
|
99
144
|
num_questions=len(true_questions) if true_questions else None
|
@@ -101,8 +146,9 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
101
146
|
# the RuleCollection needs to be present while we add the questions; we might override this later
|
102
147
|
# if a rule_collection is provided. This allows us to serialize the survey with the rule_collection.
|
103
148
|
|
149
|
+
# this is where the Questions constructor is called.
|
104
150
|
self.questions = true_questions
|
105
|
-
self.instruction_names_to_instructions = instruction_names_to_instructions
|
151
|
+
# self.instruction_names_to_instructions = instruction_names_to_instructions
|
106
152
|
|
107
153
|
self.memory_plan = memory_plan or MemoryPlan(self)
|
108
154
|
if question_groups is not None:
|
@@ -110,7 +156,7 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
110
156
|
else:
|
111
157
|
self.question_groups = {}
|
112
158
|
|
113
|
-
# if a rule collection is provided, use it instead
|
159
|
+
# if a rule collection is provided, use it instead of the constructed one
|
114
160
|
if rule_collection is not None:
|
115
161
|
self.rule_collection = rule_collection
|
116
162
|
|
@@ -119,97 +165,58 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
119
165
|
|
120
166
|
warnings.warn("name parameter to a survey is deprecated.")
|
121
167
|
|
122
|
-
|
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
|
123
206
|
@property
|
124
|
-
def
|
207
|
+
def _relevant_instructions_dict(self) -> InstructionCollection:
|
125
208
|
"""Return a dictionary with keys as question names and values as instructions that are relevant to the question.
|
126
209
|
|
127
210
|
>>> s = Survey.example(include_instructions=True)
|
128
|
-
>>> s.
|
211
|
+
>>> s._relevant_instructions_dict
|
129
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!")]}
|
130
213
|
|
131
214
|
"""
|
132
215
|
return InstructionCollection(
|
133
|
-
self.
|
216
|
+
self._instruction_names_to_instructions, self.questions
|
134
217
|
)
|
135
218
|
|
136
|
-
|
137
|
-
def _separate_questions_and_instructions(questions_and_instructions: list) -> tuple:
|
138
|
-
"""
|
139
|
-
The 'pseudo_indices' attribute is a dictionary that maps question names to pseudo-indices
|
140
|
-
that are used to order questions and instructions in the survey.
|
141
|
-
Only questions get real indices; instructions get pseudo-indices.
|
142
|
-
However, the order of the pseudo-indices is the same as the order questions and instructions are added to the survey.
|
143
|
-
|
144
|
-
We don't have to know how many instructions there are to calculate the pseudo-indices because they are
|
145
|
-
calculated by the inverse of one minus the sum of 1/2^n for n in the number of instructions run so far.
|
146
|
-
|
147
|
-
>>> from edsl import Instruction
|
148
|
-
>>> i = Instruction(text = "Pay attention to the following questions.", name = "intro")
|
149
|
-
>>> i2 = Instruction(text = "How are you feeling today?", name = "followon_intro")
|
150
|
-
>>> from edsl import QuestionFreeText; q1 = QuestionFreeText.example()
|
151
|
-
>>> from edsl import QuestionMultipleChoice; q2 = QuestionMultipleChoice.example()
|
152
|
-
>>> s = Survey([q1, i, i2, q2])
|
153
|
-
>>> len(s.instruction_names_to_instructions)
|
154
|
-
2
|
155
|
-
>>> s.pseudo_indices
|
156
|
-
{'how_are_you': 0, 'intro': 0.5, 'followon_intro': 0.75, 'how_feeling': 1}
|
157
|
-
|
158
|
-
>>> from edsl import ChangeInstruction
|
159
|
-
>>> q3 = QuestionFreeText(question_text = "What is your favorite color?", question_name = "color")
|
160
|
-
>>> i_change = ChangeInstruction(drop = ["intro"])
|
161
|
-
>>> s = Survey([q1, i, q2, i_change, q3])
|
162
|
-
>>> [i.name for i in s.relevant_instructions(q1)]
|
163
|
-
[]
|
164
|
-
>>> [i.name for i in s.relevant_instructions(q2)]
|
165
|
-
['intro']
|
166
|
-
>>> [i.name for i in s.relevant_instructions(q3)]
|
167
|
-
[]
|
168
|
-
|
169
|
-
>>> i_change = ChangeInstruction(keep = ["poop"], drop = [])
|
170
|
-
>>> s = Survey([q1, i, q2, i_change])
|
171
|
-
Traceback (most recent call last):
|
172
|
-
...
|
173
|
-
ValueError: ChangeInstruction change_instruction_0 references instruction poop which does not exist.
|
174
|
-
"""
|
175
|
-
from edsl.surveys.instructions.Instruction import Instruction
|
176
|
-
from edsl.surveys.instructions.ChangeInstruction import ChangeInstruction
|
177
|
-
|
178
|
-
true_questions = []
|
179
|
-
instruction_names_to_instructions = {}
|
180
|
-
|
181
|
-
num_change_instructions = 0
|
182
|
-
pseudo_indices = {}
|
183
|
-
instructions_run_length = 0
|
184
|
-
for entry in questions_and_instructions:
|
185
|
-
if isinstance(entry, Instruction) or isinstance(entry, ChangeInstruction):
|
186
|
-
if isinstance(entry, ChangeInstruction):
|
187
|
-
entry.add_name(num_change_instructions)
|
188
|
-
num_change_instructions += 1
|
189
|
-
for prior_instruction in entry.keep + entry.drop:
|
190
|
-
if prior_instruction not in instruction_names_to_instructions:
|
191
|
-
raise ValueError(
|
192
|
-
f"ChangeInstruction {entry.name} references instruction {prior_instruction} which does not exist."
|
193
|
-
)
|
194
|
-
instructions_run_length += 1
|
195
|
-
delta = 1 - 1.0 / (2.0**instructions_run_length)
|
196
|
-
pseudo_index = (len(true_questions) - 1) + delta
|
197
|
-
entry.pseudo_index = pseudo_index
|
198
|
-
instruction_names_to_instructions[entry.name] = entry
|
199
|
-
elif isinstance(entry, QuestionBase):
|
200
|
-
pseudo_index = len(true_questions)
|
201
|
-
instructions_run_length = 0
|
202
|
-
true_questions.append(entry)
|
203
|
-
else:
|
204
|
-
raise ValueError(
|
205
|
-
f"Entry {repr(entry)} is not a QuestionBase or an Instruction."
|
206
|
-
)
|
207
|
-
|
208
|
-
pseudo_indices[entry.name] = pseudo_index
|
209
|
-
|
210
|
-
return true_questions, instruction_names_to_instructions, pseudo_indices
|
211
|
-
|
212
|
-
def relevant_instructions(self, question) -> dict:
|
219
|
+
def _relevant_instructions(self, question: QuestionBase) -> dict:
|
213
220
|
"""This should be a dictionry with keys as question names and values as instructions that are relevant to the question.
|
214
221
|
|
215
222
|
:param question: The question to get the relevant instructions for.
|
@@ -217,38 +224,13 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
217
224
|
# Did the instruction come before the question and was it not modified by a change instruction?
|
218
225
|
|
219
226
|
"""
|
220
|
-
return
|
221
|
-
|
222
|
-
|
223
|
-
def max_pseudo_index(self) -> float:
|
224
|
-
"""Return the maximum pseudo index in the survey.
|
225
|
-
|
226
|
-
Example:
|
227
|
-
|
228
|
-
>>> s = Survey.example()
|
229
|
-
>>> s.max_pseudo_index
|
230
|
-
2
|
231
|
-
"""
|
232
|
-
if len(self.pseudo_indices) == 0:
|
233
|
-
return -1
|
234
|
-
return max(self.pseudo_indices.values())
|
235
|
-
|
236
|
-
@property
|
237
|
-
def last_item_was_instruction(self) -> bool:
|
238
|
-
"""Return whether the last item added to the survey was an instruction.
|
239
|
-
This is used to determine the pseudo-index of the next item added to the survey.
|
240
|
-
|
241
|
-
Example:
|
227
|
+
return InstructionCollection(
|
228
|
+
self._instruction_names_to_instructions, self.questions
|
229
|
+
)[question]
|
242
230
|
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
>>> from edsl.surveys.instructions.Instruction import Instruction
|
247
|
-
>>> s = s.add_instruction(Instruction(text="Pay attention to the following questions.", name="intro"))
|
248
|
-
>>> s.last_item_was_instruction
|
249
|
-
True
|
250
|
-
"""
|
251
|
-
return isinstance(self.max_pseudo_index, float)
|
231
|
+
def show_flow(self, filename: Optional[str] = None) -> None:
|
232
|
+
"""Show the flow of the survey."""
|
233
|
+
SurveyFlowVisualization(self).show_flow(filename=filename)
|
252
234
|
|
253
235
|
def add_instruction(
|
254
236
|
self, instruction: Union["Instruction", "ChangeInstruction"]
|
@@ -261,101 +243,21 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
261
243
|
>>> from edsl import Instruction
|
262
244
|
>>> i = Instruction(text="Pay attention to the following questions.", name="intro")
|
263
245
|
>>> s = Survey().add_instruction(i)
|
264
|
-
>>> s.
|
246
|
+
>>> s._instruction_names_to_instructions
|
265
247
|
{'intro': Instruction(name="intro", text="Pay attention to the following questions.")}
|
266
|
-
>>> s.
|
248
|
+
>>> s._pseudo_indices
|
267
249
|
{'intro': -0.5}
|
268
250
|
"""
|
269
|
-
|
270
|
-
|
271
|
-
if instruction.name in self.instruction_names_to_instructions:
|
272
|
-
raise SurveyCreationError(
|
273
|
-
f"""Instruction name '{instruction.name}' already exists in survey. Existing names are {self.instruction_names_to_instructions.keys()}."""
|
274
|
-
)
|
275
|
-
self.instruction_names_to_instructions[instruction.name] = instruction
|
276
|
-
|
277
|
-
# was the last thing added an instruction or a question?
|
278
|
-
if self.last_item_was_instruction:
|
279
|
-
pseudo_index = (
|
280
|
-
self.max_pseudo_index
|
281
|
-
+ (math.ceil(self.max_pseudo_index) - self.max_pseudo_index) / 2
|
282
|
-
)
|
283
|
-
else:
|
284
|
-
pseudo_index = self.max_pseudo_index + 1.0 / 2.0
|
285
|
-
self.pseudo_indices[instruction.name] = pseudo_index
|
286
|
-
|
287
|
-
return self
|
251
|
+
return EditSurvey(self).add_instruction(instruction)
|
288
252
|
|
289
253
|
# endregion
|
290
|
-
|
291
|
-
# region: Simulation methods
|
292
|
-
|
293
254
|
@classmethod
|
294
|
-
def random_survey(
|
295
|
-
|
296
|
-
from edsl.questions import QuestionMultipleChoice, QuestionFreeText
|
297
|
-
from random import choice
|
298
|
-
|
299
|
-
num_questions = 10
|
300
|
-
questions = []
|
301
|
-
for i in range(num_questions):
|
302
|
-
if choice([True, False]):
|
303
|
-
q = QuestionMultipleChoice(
|
304
|
-
question_text="nothing",
|
305
|
-
question_name="q_" + str(i),
|
306
|
-
question_options=list(range(3)),
|
307
|
-
)
|
308
|
-
questions.append(q)
|
309
|
-
else:
|
310
|
-
questions.append(
|
311
|
-
QuestionFreeText(
|
312
|
-
question_text="nothing", question_name="q_" + str(i)
|
313
|
-
)
|
314
|
-
)
|
315
|
-
s = Survey(questions)
|
316
|
-
start_index = choice(range(num_questions - 1))
|
317
|
-
end_index = choice(range(start_index + 1, 10))
|
318
|
-
s = s.add_rule(f"q_{start_index}", "True", f"q_{end_index}")
|
319
|
-
question_to_delete = choice(range(num_questions))
|
320
|
-
s.delete_question(f"q_{question_to_delete}")
|
321
|
-
return s
|
255
|
+
def random_survey(cls):
|
256
|
+
return Simulator.random_survey()
|
322
257
|
|
323
258
|
def simulate(self) -> dict:
|
324
259
|
"""Simulate the survey and return the answers."""
|
325
|
-
|
326
|
-
q = next(i)
|
327
|
-
num_passes = 0
|
328
|
-
while True:
|
329
|
-
num_passes += 1
|
330
|
-
try:
|
331
|
-
answer = q._simulate_answer()
|
332
|
-
q = i.send({q.question_name: answer["answer"]})
|
333
|
-
except StopIteration:
|
334
|
-
break
|
335
|
-
|
336
|
-
if num_passes > 100:
|
337
|
-
print("Too many passes.")
|
338
|
-
raise Exception("Too many passes.")
|
339
|
-
return self.answers
|
340
|
-
|
341
|
-
def create_agent(self) -> "Agent":
|
342
|
-
"""Create an agent from the simulated answers."""
|
343
|
-
answers_dict = self.simulate()
|
344
|
-
|
345
|
-
def construct_answer_dict_function(traits: dict) -> Callable:
|
346
|
-
def func(self, question: "QuestionBase", scenario=None):
|
347
|
-
return traits.get(question.question_name, None)
|
348
|
-
|
349
|
-
return func
|
350
|
-
|
351
|
-
return Agent(traits=answers_dict).add_direct_question_answering_method(
|
352
|
-
construct_answer_dict_function(answers_dict)
|
353
|
-
)
|
354
|
-
|
355
|
-
def simulate_results(self) -> "Results":
|
356
|
-
"""Simulate the survey and return the results."""
|
357
|
-
a = self.create_agent()
|
358
|
-
return self.by([a]).run()
|
260
|
+
return Simulator(self).simulate()
|
359
261
|
|
360
262
|
# endregion
|
361
263
|
|
@@ -391,26 +293,19 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
391
293
|
)
|
392
294
|
return self.question_name_to_index[question_name]
|
393
295
|
|
394
|
-
def
|
296
|
+
def _get_question_by_name(self, question_name: str) -> QuestionBase:
|
395
297
|
"""
|
396
298
|
Return the question object given the question name.
|
397
299
|
|
398
300
|
:param question_name: The name of the question to get.
|
399
301
|
|
400
302
|
>>> s = Survey.example()
|
401
|
-
>>> s.
|
303
|
+
>>> s._get_question_by_name("q0")
|
402
304
|
Question('multiple_choice', question_name = \"""q0\""", question_text = \"""Do you like school?\""", question_options = ['yes', 'no'])
|
403
305
|
"""
|
404
306
|
if question_name not in self.question_name_to_index:
|
405
307
|
raise SurveyError(f"Question name {question_name} not found in survey.")
|
406
|
-
|
407
|
-
return self._questions[index]
|
408
|
-
|
409
|
-
def get_question(self, question_name: str) -> QuestionBase:
|
410
|
-
"""Return the question object given the question name."""
|
411
|
-
# import warnings
|
412
|
-
# warnings.warn("survey.get_question is deprecated. Use subscript operator instead.")
|
413
|
-
return self.get(question_name)
|
308
|
+
return self._questions[self.question_name_to_index[question_name]]
|
414
309
|
|
415
310
|
def question_names_to_questions(self) -> dict:
|
416
311
|
"""Return a dictionary mapping question names to question attributes."""
|
@@ -443,12 +338,6 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
443
338
|
# endregion
|
444
339
|
|
445
340
|
# region: serialization methods
|
446
|
-
def __hash__(self) -> int:
|
447
|
-
"""Return a hash of the question."""
|
448
|
-
from edsl.utilities.utilities import dict_hash
|
449
|
-
|
450
|
-
return dict_hash(self.to_dict(add_edsl_version=False))
|
451
|
-
|
452
341
|
def to_dict(self, add_edsl_version=True) -> dict[str, Any]:
|
453
342
|
"""Serialize the Survey object to a dictionary.
|
454
343
|
|
@@ -456,10 +345,12 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
456
345
|
>>> s.to_dict(add_edsl_version = False).keys()
|
457
346
|
dict_keys(['questions', 'memory_plan', 'rule_collection', 'question_groups'])
|
458
347
|
"""
|
459
|
-
|
348
|
+
from edsl import __version__
|
349
|
+
|
350
|
+
d = {
|
460
351
|
"questions": [
|
461
352
|
q.to_dict(add_edsl_version=add_edsl_version)
|
462
|
-
for q in self.
|
353
|
+
for q in self._recombined_questions_and_instructions()
|
463
354
|
],
|
464
355
|
"memory_plan": self.memory_plan.to_dict(add_edsl_version=add_edsl_version),
|
465
356
|
"rule_collection": self.rule_collection.to_dict(
|
@@ -467,6 +358,13 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
467
358
|
),
|
468
359
|
"question_groups": self.question_groups,
|
469
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
|
470
368
|
|
471
369
|
@classmethod
|
472
370
|
@remove_edsl_version
|
@@ -489,6 +387,8 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
489
387
|
"""
|
490
388
|
|
491
389
|
def get_class(pass_dict):
|
390
|
+
from edsl.questions.QuestionBase import QuestionBase
|
391
|
+
|
492
392
|
if (class_name := pass_dict.get("edsl_class_name")) == "QuestionBase":
|
493
393
|
return QuestionBase
|
494
394
|
elif class_name == "Instruction":
|
@@ -508,11 +408,16 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
508
408
|
get_class(q_dict).from_dict(q_dict) for q_dict in data["questions"]
|
509
409
|
]
|
510
410
|
memory_plan = MemoryPlan.from_dict(data["memory_plan"])
|
411
|
+
if "questions_to_randomize" in data:
|
412
|
+
questions_to_randomize = data["questions_to_randomize"]
|
413
|
+
else:
|
414
|
+
questions_to_randomize = None
|
511
415
|
survey = cls(
|
512
416
|
questions=questions,
|
513
417
|
memory_plan=memory_plan,
|
514
418
|
rule_collection=RuleCollection.from_dict(data["rule_collection"]),
|
515
419
|
question_groups=data["question_groups"],
|
420
|
+
questions_to_randomize=questions_to_randomize,
|
516
421
|
)
|
517
422
|
return survey
|
518
423
|
|
@@ -600,27 +505,16 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
600
505
|
|
601
506
|
return Survey(questions=self.questions + other.questions)
|
602
507
|
|
603
|
-
def move_question(self, identifier: Union[str, int], new_index: int):
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
index = identifier
|
614
|
-
else:
|
615
|
-
raise SurveyError(
|
616
|
-
"Identifier must be either a string (question name) or an integer (question index)."
|
617
|
-
)
|
618
|
-
|
619
|
-
moving_question = self._questions[index]
|
620
|
-
|
621
|
-
new_survey = self.delete_question(index)
|
622
|
-
new_survey.add_question(moving_question, new_index)
|
623
|
-
return new_survey
|
508
|
+
def move_question(self, identifier: Union[str, int], new_index: int) -> Survey:
|
509
|
+
"""
|
510
|
+
>>> from edsl import QuestionMultipleChoice, Survey
|
511
|
+
>>> s = Survey.example()
|
512
|
+
>>> s.question_names
|
513
|
+
['q0', 'q1', 'q2']
|
514
|
+
>>> s.move_question("q0", 2).question_names
|
515
|
+
['q1', 'q2', 'q0']
|
516
|
+
"""
|
517
|
+
return EditSurvey(self).move_question(identifier, new_index)
|
624
518
|
|
625
519
|
def delete_question(self, identifier: Union[str, int]) -> Survey:
|
626
520
|
"""
|
@@ -640,54 +534,7 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
640
534
|
>>> len(s.questions)
|
641
535
|
0
|
642
536
|
"""
|
643
|
-
|
644
|
-
if identifier not in self.question_names:
|
645
|
-
raise SurveyError(
|
646
|
-
f"Question name '{identifier}' does not exist in the survey."
|
647
|
-
)
|
648
|
-
index = self.question_name_to_index[identifier]
|
649
|
-
elif isinstance(identifier, int):
|
650
|
-
if identifier < 0 or identifier >= len(self.questions):
|
651
|
-
raise SurveyError(f"Index {identifier} is out of range.")
|
652
|
-
index = identifier
|
653
|
-
else:
|
654
|
-
raise SurveyError(
|
655
|
-
"Identifier must be either a string (question name) or an integer (question index)."
|
656
|
-
)
|
657
|
-
|
658
|
-
# Remove the question
|
659
|
-
deleted_question = self._questions.pop(index)
|
660
|
-
del self.pseudo_indices[deleted_question.question_name]
|
661
|
-
|
662
|
-
# Update indices
|
663
|
-
for question_name, old_index in self.pseudo_indices.items():
|
664
|
-
if old_index > index:
|
665
|
-
self.pseudo_indices[question_name] = old_index - 1
|
666
|
-
|
667
|
-
# Update rules
|
668
|
-
new_rule_collection = RuleCollection()
|
669
|
-
for rule in self.rule_collection:
|
670
|
-
if rule.current_q == index:
|
671
|
-
continue # Remove rules associated with the deleted question
|
672
|
-
if rule.current_q > index:
|
673
|
-
rule.current_q -= 1
|
674
|
-
if rule.next_q > index:
|
675
|
-
rule.next_q -= 1
|
676
|
-
|
677
|
-
if rule.next_q == index:
|
678
|
-
if index == len(self.questions):
|
679
|
-
rule.next_q = EndOfSurvey
|
680
|
-
else:
|
681
|
-
rule.next_q = index
|
682
|
-
|
683
|
-
new_rule_collection.add_rule(rule)
|
684
|
-
self.rule_collection = new_rule_collection
|
685
|
-
|
686
|
-
# Update memory plan if it exists
|
687
|
-
if hasattr(self, "memory_plan"):
|
688
|
-
self.memory_plan.remove_question(deleted_question.question_name)
|
689
|
-
|
690
|
-
return self
|
537
|
+
return EditSurvey(self).delete_question(identifier)
|
691
538
|
|
692
539
|
def add_question(
|
693
540
|
self, question: QuestionBase, index: Optional[int] = None
|
@@ -711,81 +558,17 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
711
558
|
edsl.exceptions.surveys.SurveyCreationError: Question name 'q0' already exists in survey. Existing names are ['q0'].
|
712
559
|
...
|
713
560
|
"""
|
714
|
-
|
715
|
-
raise SurveyCreationError(
|
716
|
-
f"""Question name '{question.question_name}' already exists in survey. Existing names are {self.question_names}."""
|
717
|
-
)
|
718
|
-
if index is None:
|
719
|
-
index = len(self.questions)
|
561
|
+
return EditSurvey(self).add_question(question, index)
|
720
562
|
|
721
|
-
|
722
|
-
raise SurveyCreationError(
|
723
|
-
f"Index {index} is greater than the number of questions in the survey."
|
724
|
-
)
|
725
|
-
if index < 0:
|
726
|
-
raise SurveyCreationError(f"Index {index} is less than 0.")
|
727
|
-
|
728
|
-
interior_insertion = index != len(self.questions)
|
729
|
-
|
730
|
-
# index = len(self.questions)
|
731
|
-
# TODO: This is a bit ugly because the user
|
732
|
-
# doesn't "know" about _questions - it's generated by the
|
733
|
-
# descriptor.
|
734
|
-
self._questions.insert(index, question)
|
735
|
-
|
736
|
-
if interior_insertion:
|
737
|
-
for question_name, old_index in self.pseudo_indices.items():
|
738
|
-
if old_index >= index:
|
739
|
-
self.pseudo_indices[question_name] = old_index + 1
|
740
|
-
|
741
|
-
self.pseudo_indices[question.question_name] = index
|
742
|
-
|
743
|
-
## Re-do question_name to index - this is done automatically
|
744
|
-
# for question_name, old_index in self.question_name_to_index.items():
|
745
|
-
# if old_index >= index:
|
746
|
-
# self.question_name_to_index[question_name] = old_index + 1
|
747
|
-
|
748
|
-
## Need to re-do the rule collection and the indices of the questions
|
749
|
-
|
750
|
-
## If a rule is before the insertion index and next_q is also before the insertion index, no change needed.
|
751
|
-
## If the rule is before the insertion index but next_q is after the insertion index, increment the next_q by 1
|
752
|
-
## If the rule is after the insertion index, increment the current_q by 1 and the next_q by 1
|
753
|
-
|
754
|
-
# using index + 1 presumes there is a next question
|
755
|
-
if interior_insertion:
|
756
|
-
for rule in self.rule_collection:
|
757
|
-
if rule.current_q >= index:
|
758
|
-
rule.current_q += 1
|
759
|
-
if rule.next_q >= index:
|
760
|
-
rule.next_q += 1
|
761
|
-
|
762
|
-
# add a new rule
|
763
|
-
self.rule_collection.add_rule(
|
764
|
-
Rule(
|
765
|
-
current_q=index,
|
766
|
-
expression="True",
|
767
|
-
next_q=index + 1,
|
768
|
-
question_name_to_index=self.question_name_to_index,
|
769
|
-
priority=RulePriority.DEFAULT.value,
|
770
|
-
)
|
771
|
-
)
|
772
|
-
|
773
|
-
# a question might be added before the memory plan is created
|
774
|
-
# it's ok because the memory plan will be updated when it is created
|
775
|
-
if hasattr(self, "memory_plan"):
|
776
|
-
self.memory_plan.add_question(question)
|
777
|
-
|
778
|
-
return self
|
779
|
-
|
780
|
-
def recombined_questions_and_instructions(
|
563
|
+
def _recombined_questions_and_instructions(
|
781
564
|
self,
|
782
565
|
) -> list[Union[QuestionBase, "Instruction"]]:
|
783
566
|
"""Return a list of questions and instructions sorted by pseudo index."""
|
784
567
|
questions_and_instructions = self._questions + list(
|
785
|
-
self.
|
568
|
+
self._instruction_names_to_instructions.values()
|
786
569
|
)
|
787
570
|
return sorted(
|
788
|
-
questions_and_instructions, key=lambda x: self.
|
571
|
+
questions_and_instructions, key=lambda x: self._pseudo_indices[x.name]
|
789
572
|
)
|
790
573
|
|
791
574
|
# endregion
|
@@ -797,7 +580,7 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
797
580
|
>>> s = Survey.example().set_full_memory_mode()
|
798
581
|
|
799
582
|
"""
|
800
|
-
self._set_memory_plan(lambda i: self.question_names[:i])
|
583
|
+
MemoryManagement(self)._set_memory_plan(lambda i: self.question_names[:i])
|
801
584
|
return self
|
802
585
|
|
803
586
|
def set_lagged_memory(self, lags: int) -> Survey:
|
@@ -805,10 +588,12 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
805
588
|
|
806
589
|
The agent should remember the answers to the questions in the survey from the previous lags.
|
807
590
|
"""
|
808
|
-
self._set_memory_plan(
|
591
|
+
MemoryManagement(self)._set_memory_plan(
|
592
|
+
lambda i: self.question_names[max(0, i - lags) : i]
|
593
|
+
)
|
809
594
|
return self
|
810
595
|
|
811
|
-
def _set_memory_plan(self, prior_questions_func: Callable):
|
596
|
+
def _set_memory_plan(self, prior_questions_func: Callable) -> None:
|
812
597
|
"""Set memory plan based on a provided function determining prior questions.
|
813
598
|
|
814
599
|
:param prior_questions_func: A function that takes the index of the current question and returns a list of prior questions to remember.
|
@@ -817,11 +602,7 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
817
602
|
>>> s._set_memory_plan(lambda i: s.question_names[:i])
|
818
603
|
|
819
604
|
"""
|
820
|
-
|
821
|
-
self.memory_plan.add_memory_collection(
|
822
|
-
focal_question=question_name,
|
823
|
-
prior_questions=prior_questions_func(i),
|
824
|
-
)
|
605
|
+
MemoryManagement(self)._set_memory_plan(prior_questions_func)
|
825
606
|
|
826
607
|
def add_targeted_memory(
|
827
608
|
self,
|
@@ -841,20 +622,10 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
841
622
|
|
842
623
|
The agent should also remember the answers to prior_questions listed in prior_questions.
|
843
624
|
"""
|
844
|
-
|
845
|
-
|
846
|
-
]
|
847
|
-
prior_question_name = self.question_names[
|
848
|
-
self._get_question_index(prior_question)
|
849
|
-
]
|
850
|
-
|
851
|
-
self.memory_plan.add_single_memory(
|
852
|
-
focal_question=focal_question_name,
|
853
|
-
prior_question=prior_question_name,
|
625
|
+
return MemoryManagement(self).add_targeted_memory(
|
626
|
+
focal_question, prior_question
|
854
627
|
)
|
855
628
|
|
856
|
-
return self
|
857
|
-
|
858
629
|
def add_memory_collection(
|
859
630
|
self,
|
860
631
|
focal_question: Union[QuestionBase, str],
|
@@ -873,23 +644,9 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
873
644
|
>>> s.memory_plan
|
874
645
|
{'q2': Memory(prior_questions=['q0', 'q1'])}
|
875
646
|
"""
|
876
|
-
|
877
|
-
|
878
|
-
]
|
879
|
-
|
880
|
-
prior_question_names = [
|
881
|
-
self.question_names[self._get_question_index(prior_question)]
|
882
|
-
for prior_question in prior_questions
|
883
|
-
]
|
884
|
-
|
885
|
-
self.memory_plan.add_memory_collection(
|
886
|
-
focal_question=focal_question_name, prior_questions=prior_question_names
|
647
|
+
return MemoryManagement(self).add_memory_collection(
|
648
|
+
focal_question, prior_questions
|
887
649
|
)
|
888
|
-
return self
|
889
|
-
|
890
|
-
# endregion
|
891
|
-
# endregion
|
892
|
-
# endregion
|
893
650
|
|
894
651
|
# region: Question groups
|
895
652
|
def add_question_group(
|
@@ -984,16 +741,9 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
984
741
|
|
985
742
|
>>> s = Survey.example()
|
986
743
|
>>> s.show_rules()
|
987
|
-
|
988
|
-
┃ current_q ┃ expression ┃ next_q ┃ priority ┃ before_rule ┃
|
989
|
-
┡━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━┩
|
990
|
-
│ 0 │ True │ 1 │ -1 │ False │
|
991
|
-
│ 0 │ q0 == 'yes' │ 2 │ 0 │ False │
|
992
|
-
│ 1 │ True │ 2 │ -1 │ False │
|
993
|
-
│ 2 │ True │ 3 │ -1 │ False │
|
994
|
-
└───────────┴─────────────┴────────┴──────────┴─────────────┘
|
744
|
+
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]}])
|
995
745
|
"""
|
996
|
-
self.rule_collection.show_rules()
|
746
|
+
return self.rule_collection.show_rules()
|
997
747
|
|
998
748
|
def add_stop_rule(
|
999
749
|
self, question: Union[QuestionBase, str], expression: str
|
@@ -1023,41 +773,15 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1023
773
|
edsl.exceptions.surveys.SurveyCreationError: The expression contains '<>', which is not allowed. You probably mean '!='.
|
1024
774
|
...
|
1025
775
|
"""
|
1026
|
-
|
1027
|
-
prior_question_appears = False
|
1028
|
-
for prior_question in self.questions:
|
1029
|
-
if prior_question.question_name in expression:
|
1030
|
-
prior_question_appears = True
|
1031
|
-
|
1032
|
-
if not prior_question_appears:
|
1033
|
-
import warnings
|
1034
|
-
|
1035
|
-
warnings.warn(
|
1036
|
-
f"The expression {expression} does not contain any prior question names. This is probably a mistake."
|
1037
|
-
)
|
1038
|
-
self.add_rule(question, expression, EndOfSurvey)
|
1039
|
-
return self
|
776
|
+
return RuleManager(self).add_stop_rule(question, expression)
|
1040
777
|
|
1041
778
|
def clear_non_default_rules(self) -> Survey:
|
1042
779
|
"""Remove all non-default rules from the survey.
|
1043
780
|
|
1044
781
|
>>> Survey.example().show_rules()
|
1045
|
-
|
1046
|
-
┃ current_q ┃ expression ┃ next_q ┃ priority ┃ before_rule ┃
|
1047
|
-
┡━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━┩
|
1048
|
-
│ 0 │ True │ 1 │ -1 │ False │
|
1049
|
-
│ 0 │ q0 == 'yes' │ 2 │ 0 │ False │
|
1050
|
-
│ 1 │ True │ 2 │ -1 │ False │
|
1051
|
-
│ 2 │ True │ 3 │ -1 │ False │
|
1052
|
-
└───────────┴─────────────┴────────┴──────────┴─────────────┘
|
782
|
+
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]}])
|
1053
783
|
>>> Survey.example().clear_non_default_rules().show_rules()
|
1054
|
-
|
1055
|
-
┃ current_q ┃ expression ┃ next_q ┃ priority ┃ before_rule ┃
|
1056
|
-
┡━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━┩
|
1057
|
-
│ 0 │ True │ 1 │ -1 │ False │
|
1058
|
-
│ 1 │ True │ 2 │ -1 │ False │
|
1059
|
-
│ 2 │ True │ 3 │ -1 │ False │
|
1060
|
-
└───────────┴────────────┴────────┴──────────┴─────────────┘
|
784
|
+
Dataset([{'current_q': [0, 1, 2]}, {'expression': ['True', 'True', 'True']}, {'next_q': [1, 2, 3]}, {'priority': [-1, -1, -1]}, {'before_rule': [False, False, False]}])
|
1061
785
|
"""
|
1062
786
|
s = Survey()
|
1063
787
|
for question in self.questions:
|
@@ -1088,38 +812,9 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1088
812
|
|
1089
813
|
"""
|
1090
814
|
question_index = self._get_question_index(question)
|
1091
|
-
self.
|
1092
|
-
|
1093
|
-
|
1094
|
-
def _get_new_rule_priority(
|
1095
|
-
self, question_index: int, before_rule: bool = False
|
1096
|
-
) -> int:
|
1097
|
-
"""Return the priority for the new rule.
|
1098
|
-
|
1099
|
-
:param question_index: The index of the question to add the rule to.
|
1100
|
-
:param before_rule: Whether the rule is evaluated before the question is answered.
|
1101
|
-
|
1102
|
-
>>> s = Survey.example()
|
1103
|
-
>>> s._get_new_rule_priority(0)
|
1104
|
-
1
|
1105
|
-
"""
|
1106
|
-
current_priorities = [
|
1107
|
-
rule.priority
|
1108
|
-
for rule in self.rule_collection.applicable_rules(
|
1109
|
-
question_index, before_rule
|
1110
|
-
)
|
1111
|
-
]
|
1112
|
-
if len(current_priorities) == 0:
|
1113
|
-
return RulePriority.DEFAULT.value + 1
|
1114
|
-
|
1115
|
-
max_priority = max(current_priorities)
|
1116
|
-
# newer rules take priority over older rules
|
1117
|
-
new_priority = (
|
1118
|
-
RulePriority.DEFAULT.value
|
1119
|
-
if len(current_priorities) == 0
|
1120
|
-
else max_priority + 1
|
815
|
+
return RuleManager(self).add_rule(
|
816
|
+
question, expression, question_index + 1, before_rule=True
|
1121
817
|
)
|
1122
|
-
return new_priority
|
1123
818
|
|
1124
819
|
def add_rule(
|
1125
820
|
self,
|
@@ -1143,52 +838,10 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1143
838
|
'q2'
|
1144
839
|
|
1145
840
|
"""
|
1146
|
-
return self.
|
841
|
+
return RuleManager(self).add_rule(
|
1147
842
|
question, expression, next_question, before_rule=before_rule
|
1148
843
|
)
|
1149
844
|
|
1150
|
-
def _add_rule(
|
1151
|
-
self,
|
1152
|
-
question: Union[QuestionBase, str],
|
1153
|
-
expression: str,
|
1154
|
-
next_question: Union[QuestionBase, str, int],
|
1155
|
-
before_rule: bool = False,
|
1156
|
-
) -> Survey:
|
1157
|
-
"""
|
1158
|
-
Add a rule to a Question of the Survey with the appropriate priority.
|
1159
|
-
|
1160
|
-
:param question: The question to add the rule to.
|
1161
|
-
:param expression: The expression to evaluate.
|
1162
|
-
:param next_question: The next question to go to if the rule is true.
|
1163
|
-
:param before_rule: Whether the rule is evaluated before the question is answered.
|
1164
|
-
|
1165
|
-
|
1166
|
-
- The last rule added for the question will have the highest priority.
|
1167
|
-
- If there are no rules, the rule added gets priority -1.
|
1168
|
-
"""
|
1169
|
-
question_index = self._get_question_index(question)
|
1170
|
-
|
1171
|
-
# Might not have the name of the next question yet
|
1172
|
-
if isinstance(next_question, int):
|
1173
|
-
next_question_index = next_question
|
1174
|
-
else:
|
1175
|
-
next_question_index = self._get_question_index(next_question)
|
1176
|
-
|
1177
|
-
new_priority = self._get_new_rule_priority(question_index, before_rule)
|
1178
|
-
|
1179
|
-
self.rule_collection.add_rule(
|
1180
|
-
Rule(
|
1181
|
-
current_q=question_index,
|
1182
|
-
expression=expression,
|
1183
|
-
next_q=next_question_index,
|
1184
|
-
question_name_to_index=self.question_name_to_index,
|
1185
|
-
priority=new_priority,
|
1186
|
-
before_rule=before_rule,
|
1187
|
-
)
|
1188
|
-
)
|
1189
|
-
|
1190
|
-
return self
|
1191
|
-
|
1192
845
|
# endregion
|
1193
846
|
|
1194
847
|
# region: Forward methods
|
@@ -1199,22 +852,26 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1199
852
|
|
1200
853
|
This takes the survey and adds an Agent and a Scenario via 'by' which converts to a Jobs object:
|
1201
854
|
|
1202
|
-
>>> s = Survey.example(); from edsl import Agent; from edsl import Scenario
|
855
|
+
>>> s = Survey.example(); from edsl.agents import Agent; from edsl import Scenario
|
1203
856
|
>>> s.by(Agent.example()).by(Scenario.example())
|
1204
857
|
Jobs(...)
|
1205
858
|
"""
|
1206
859
|
from edsl.jobs.Jobs import Jobs
|
1207
860
|
|
1208
|
-
|
1209
|
-
return job.by(*args)
|
861
|
+
return Jobs(survey=self).by(*args)
|
1210
862
|
|
1211
863
|
def to_jobs(self):
|
1212
|
-
"""Convert the survey to a Jobs object.
|
864
|
+
"""Convert the survey to a Jobs object.
|
865
|
+
>>> s = Survey.example()
|
866
|
+
>>> s.to_jobs()
|
867
|
+
Jobs(...)
|
868
|
+
"""
|
1213
869
|
from edsl.jobs.Jobs import Jobs
|
1214
870
|
|
1215
871
|
return Jobs(survey=self)
|
1216
872
|
|
1217
873
|
def show_prompts(self):
|
874
|
+
"""Show the prompts for the survey."""
|
1218
875
|
return self.to_jobs().show_prompts()
|
1219
876
|
|
1220
877
|
# endregion
|
@@ -1226,6 +883,7 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1226
883
|
model=None,
|
1227
884
|
agent=None,
|
1228
885
|
cache=None,
|
886
|
+
verbose=False,
|
1229
887
|
disable_remote_cache: bool = False,
|
1230
888
|
disable_remote_inference: bool = False,
|
1231
889
|
**kwargs,
|
@@ -1241,19 +899,21 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1241
899
|
>>> s(period = "evening", cache = False, disable_remote_cache = True, disable_remote_inference = True).select("answer.q0").first()
|
1242
900
|
'no'
|
1243
901
|
"""
|
1244
|
-
|
1245
|
-
return
|
902
|
+
|
903
|
+
return self.get_job(model, agent, **kwargs).run(
|
1246
904
|
cache=cache,
|
905
|
+
verbose=verbose,
|
1247
906
|
disable_remote_cache=disable_remote_cache,
|
1248
907
|
disable_remote_inference=disable_remote_inference,
|
1249
908
|
)
|
1250
909
|
|
1251
910
|
async def run_async(
|
1252
911
|
self,
|
1253
|
-
model: Optional["
|
912
|
+
model: Optional["LanguageModel"] = None,
|
1254
913
|
agent: Optional["Agent"] = None,
|
1255
914
|
cache: Optional["Cache"] = None,
|
1256
915
|
disable_remote_inference: bool = False,
|
916
|
+
disable_remote_cache: bool = False,
|
1257
917
|
**kwargs,
|
1258
918
|
):
|
1259
919
|
"""Run the survey with default model, taking the required survey as arguments.
|
@@ -1263,7 +923,7 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1263
923
|
>>> def f(scenario, agent_traits): return "yes" if scenario["period"] == "morning" else "no"
|
1264
924
|
>>> q = QuestionFunctional(question_name = "q0", func = f)
|
1265
925
|
>>> s = Survey([q])
|
1266
|
-
>>> async def test_run_async(): result = await s.run_async(period="morning", disable_remote_inference = True); print(result.select("answer.q0").first())
|
926
|
+
>>> 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())
|
1267
927
|
>>> asyncio.run(test_run_async())
|
1268
928
|
yes
|
1269
929
|
>>> import asyncio
|
@@ -1271,20 +931,23 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1271
931
|
>>> def f(scenario, agent_traits): return "yes" if scenario["period"] == "morning" else "no"
|
1272
932
|
>>> q = QuestionFunctional(question_name = "q0", func = f)
|
1273
933
|
>>> s = Survey([q])
|
1274
|
-
>>> async def test_run_async(): result = await s.run_async(period="evening", disable_remote_inference = True); print(result.select("answer.q0").first())
|
1275
|
-
>>> asyncio.run(test_run_async())
|
934
|
+
>>> 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())
|
935
|
+
>>> results = asyncio.run(test_run_async())
|
1276
936
|
no
|
1277
937
|
"""
|
1278
938
|
# TODO: temp fix by creating a cache
|
1279
939
|
if cache is None:
|
1280
940
|
from edsl.data import Cache
|
1281
|
-
|
1282
941
|
c = Cache()
|
1283
942
|
else:
|
1284
943
|
c = cache
|
1285
|
-
|
944
|
+
|
945
|
+
|
946
|
+
|
947
|
+
jobs: "Jobs" = self.get_job(model=model, agent=agent, **kwargs).using(c)
|
1286
948
|
return await jobs.run_async(
|
1287
|
-
|
949
|
+
disable_remote_inference=disable_remote_inference,
|
950
|
+
disable_remote_cache=disable_remote_cache,
|
1288
951
|
)
|
1289
952
|
|
1290
953
|
def run(self, *args, **kwargs) -> "Results":
|
@@ -1302,9 +965,30 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1302
965
|
|
1303
966
|
return Jobs(survey=self).run(*args, **kwargs)
|
1304
967
|
|
968
|
+
def using(self, obj: Union["Cache", "KeyLookup", "BucketCollection"]) -> "Jobs":
|
969
|
+
"""Turn the survey into a Job and appends the arguments to the Job."""
|
970
|
+
from edsl.jobs.Jobs import Jobs
|
971
|
+
|
972
|
+
return Jobs(survey=self).using(obj)
|
973
|
+
|
974
|
+
def duplicate(self):
|
975
|
+
"""Duplicate the survey.
|
976
|
+
|
977
|
+
>>> s = Survey.example()
|
978
|
+
>>> s2 = s.duplicate()
|
979
|
+
>>> s == s2
|
980
|
+
True
|
981
|
+
>>> s is s2
|
982
|
+
False
|
983
|
+
|
984
|
+
"""
|
985
|
+
return Survey.from_dict(self.to_dict())
|
986
|
+
|
1305
987
|
# region: Survey flow
|
1306
988
|
def next_question(
|
1307
|
-
self,
|
989
|
+
self,
|
990
|
+
current_question: Optional[Union[str, QuestionBase]] = None,
|
991
|
+
answers: Optional[dict] = None,
|
1308
992
|
) -> Union[QuestionBase, EndOfSurvey.__class__]:
|
1309
993
|
"""
|
1310
994
|
Return the next question in a survey.
|
@@ -1323,8 +1007,11 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1323
1007
|
'q1'
|
1324
1008
|
|
1325
1009
|
"""
|
1010
|
+
if current_question is None:
|
1011
|
+
return self.questions[0]
|
1012
|
+
|
1326
1013
|
if isinstance(current_question, str):
|
1327
|
-
current_question = self.
|
1014
|
+
current_question = self._get_question_by_name(current_question)
|
1328
1015
|
|
1329
1016
|
question_index = self.question_name_to_index[current_question.question_name]
|
1330
1017
|
next_question_object = self.rule_collection.next_question(
|
@@ -1354,14 +1041,7 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1354
1041
|
|
1355
1042
|
>>> s = Survey.example()
|
1356
1043
|
>>> s.show_rules()
|
1357
|
-
|
1358
|
-
┃ current_q ┃ expression ┃ next_q ┃ priority ┃ before_rule ┃
|
1359
|
-
┡━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━┩
|
1360
|
-
│ 0 │ True │ 1 │ -1 │ False │
|
1361
|
-
│ 0 │ q0 == 'yes' │ 2 │ 0 │ False │
|
1362
|
-
│ 1 │ True │ 2 │ -1 │ False │
|
1363
|
-
│ 2 │ True │ 3 │ -1 │ False │
|
1364
|
-
└───────────┴─────────────┴────────┴──────────┴─────────────┘
|
1044
|
+
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]}])
|
1365
1045
|
|
1366
1046
|
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.
|
1367
1047
|
|
@@ -1390,7 +1070,6 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1390
1070
|
question = self.next_question(question, self.answers)
|
1391
1071
|
|
1392
1072
|
while not question == EndOfSurvey:
|
1393
|
-
# breakpoint()
|
1394
1073
|
answer = yield question
|
1395
1074
|
self.answers.update(answer)
|
1396
1075
|
# print(f"Answers: {self.answers}")
|
@@ -1399,69 +1078,6 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1399
1078
|
|
1400
1079
|
# endregion
|
1401
1080
|
|
1402
|
-
# regions: DAG construction
|
1403
|
-
def textify(self, index_dag: DAG) -> DAG:
|
1404
|
-
"""Convert the DAG of question indices to a DAG of question names.
|
1405
|
-
|
1406
|
-
:param index_dag: The DAG of question indices.
|
1407
|
-
|
1408
|
-
Example:
|
1409
|
-
|
1410
|
-
>>> s = Survey.example()
|
1411
|
-
>>> d = s.dag()
|
1412
|
-
>>> d
|
1413
|
-
{1: {0}, 2: {0}}
|
1414
|
-
>>> s.textify(d)
|
1415
|
-
{'q1': {'q0'}, 'q2': {'q0'}}
|
1416
|
-
"""
|
1417
|
-
|
1418
|
-
def get_name(index: int):
|
1419
|
-
"""Return the name of the question given the index."""
|
1420
|
-
if index >= len(self.questions):
|
1421
|
-
return EndOfSurvey
|
1422
|
-
try:
|
1423
|
-
return self.questions[index].question_name
|
1424
|
-
except IndexError:
|
1425
|
-
print(
|
1426
|
-
f"The index is {index} but the length of the questions is {len(self.questions)}"
|
1427
|
-
)
|
1428
|
-
raise SurveyError
|
1429
|
-
|
1430
|
-
try:
|
1431
|
-
text_dag = {}
|
1432
|
-
for child_index, parent_indices in index_dag.items():
|
1433
|
-
parent_names = {get_name(index) for index in parent_indices}
|
1434
|
-
child_name = get_name(child_index)
|
1435
|
-
text_dag[child_name] = parent_names
|
1436
|
-
return text_dag
|
1437
|
-
except IndexError:
|
1438
|
-
raise
|
1439
|
-
|
1440
|
-
@property
|
1441
|
-
def piping_dag(self) -> DAG:
|
1442
|
-
"""Figures out the DAG of piping dependencies.
|
1443
|
-
|
1444
|
-
>>> from edsl import QuestionFreeText
|
1445
|
-
>>> q0 = QuestionFreeText(question_text="Here is a question", question_name="q0")
|
1446
|
-
>>> q1 = QuestionFreeText(question_text="You previously answered {{ q0 }}---how do you feel now?", question_name="q1")
|
1447
|
-
>>> s = Survey([q0, q1])
|
1448
|
-
>>> s.piping_dag
|
1449
|
-
{1: {0}}
|
1450
|
-
"""
|
1451
|
-
d = {}
|
1452
|
-
for question_name, depenencies in self.parameters_by_question.items():
|
1453
|
-
if depenencies:
|
1454
|
-
question_index = self.question_name_to_index[question_name]
|
1455
|
-
for dependency in depenencies:
|
1456
|
-
if dependency not in self.question_name_to_index:
|
1457
|
-
pass
|
1458
|
-
else:
|
1459
|
-
dependency_index = self.question_name_to_index[dependency]
|
1460
|
-
if question_index not in d:
|
1461
|
-
d[question_index] = set()
|
1462
|
-
d[question_index].add(dependency_index)
|
1463
|
-
return d
|
1464
|
-
|
1465
1081
|
def dag(self, textify: bool = False) -> DAG:
|
1466
1082
|
"""Return the DAG of the survey, which reflects both skip-logic and memory.
|
1467
1083
|
|
@@ -1473,14 +1089,9 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1473
1089
|
{1: {0}, 2: {0}}
|
1474
1090
|
|
1475
1091
|
"""
|
1476
|
-
|
1477
|
-
|
1478
|
-
|
1479
|
-
if textify:
|
1480
|
-
memory_dag = DAG(self.textify(memory_dag))
|
1481
|
-
rule_dag = DAG(self.textify(rule_dag))
|
1482
|
-
piping_dag = DAG(self.textify(piping_dag))
|
1483
|
-
return memory_dag + rule_dag + piping_dag
|
1092
|
+
from edsl.surveys.ConstructDAG import ConstructDAG
|
1093
|
+
|
1094
|
+
return ConstructDAG(self).dag(textify)
|
1484
1095
|
|
1485
1096
|
###################
|
1486
1097
|
# DUNDER METHODS
|
@@ -1509,77 +1120,18 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1509
1120
|
elif isinstance(index, str):
|
1510
1121
|
return getattr(self, index)
|
1511
1122
|
|
1512
|
-
def _diff(self, other):
|
1513
|
-
|
1514
|
-
|
1515
|
-
|
1516
|
-
for key, value in self.to_dict().items():
|
1517
|
-
if value != other.to_dict()[key]:
|
1518
|
-
print(f"Key: {key}")
|
1519
|
-
print("\n")
|
1520
|
-
print(f"Self: {value}")
|
1521
|
-
print("\n")
|
1522
|
-
print(f"Other: {other.to_dict()[key]}")
|
1523
|
-
print("\n\n")
|
1524
|
-
|
1525
|
-
def __eq__(self, other) -> bool:
|
1526
|
-
"""Return True if the two surveys have the same to_dict.
|
1527
|
-
|
1528
|
-
:param other: The other survey to compare to.
|
1529
|
-
|
1530
|
-
>>> s = Survey.example()
|
1531
|
-
>>> s == s
|
1532
|
-
True
|
1533
|
-
|
1534
|
-
>>> s == "poop"
|
1535
|
-
False
|
1536
|
-
|
1537
|
-
"""
|
1538
|
-
if not isinstance(other, Survey):
|
1539
|
-
return False
|
1540
|
-
return self.to_dict() == other.to_dict()
|
1541
|
-
|
1542
|
-
@classmethod
|
1543
|
-
def from_qsf(
|
1544
|
-
cls, qsf_file: Optional[str] = None, url: Optional[str] = None
|
1545
|
-
) -> Survey:
|
1546
|
-
"""Create a Survey object from a Qualtrics QSF file."""
|
1123
|
+
# def _diff(self, other):
|
1124
|
+
# """Used for debugging. Print out the differences between two surveys."""
|
1125
|
+
# from rich import print
|
1547
1126
|
|
1548
|
-
|
1549
|
-
|
1550
|
-
|
1551
|
-
|
1552
|
-
|
1553
|
-
|
1554
|
-
|
1555
|
-
|
1556
|
-
response.raise_for_status() # Ensure the request was successful
|
1557
|
-
|
1558
|
-
# Save the Excel file to a temporary file
|
1559
|
-
with tempfile.NamedTemporaryFile(suffix=".qsf", delete=False) as temp_file:
|
1560
|
-
temp_file.write(response.content)
|
1561
|
-
qsf_file = temp_file.name
|
1562
|
-
|
1563
|
-
from edsl.surveys.SurveyQualtricsImport import SurveyQualtricsImport
|
1564
|
-
|
1565
|
-
so = SurveyQualtricsImport(qsf_file)
|
1566
|
-
return so.create_survey()
|
1567
|
-
|
1568
|
-
# region: Display methods
|
1569
|
-
def print(self):
|
1570
|
-
"""Print the survey in a rich format.
|
1571
|
-
|
1572
|
-
>>> s = Survey.example()
|
1573
|
-
>>> s.print()
|
1574
|
-
{
|
1575
|
-
"questions": [
|
1576
|
-
...
|
1577
|
-
}
|
1578
|
-
"""
|
1579
|
-
from rich import print_json
|
1580
|
-
import json
|
1581
|
-
|
1582
|
-
print_json(json.dumps(self.to_dict()))
|
1127
|
+
# for key, value in self.to_dict().items():
|
1128
|
+
# if value != other.to_dict()[key]:
|
1129
|
+
# print(f"Key: {key}")
|
1130
|
+
# print("\n")
|
1131
|
+
# print(f"Self: {value}")
|
1132
|
+
# print("\n")
|
1133
|
+
# print(f"Other: {other.to_dict()[key]}")
|
1134
|
+
# print("\n\n")
|
1583
1135
|
|
1584
1136
|
def __repr__(self) -> str:
|
1585
1137
|
"""Return a string representation of the survey."""
|
@@ -1587,60 +1139,20 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1587
1139
|
# questions_string = ", ".join([repr(q) for q in self._questions])
|
1588
1140
|
questions_string = ", ".join([repr(q) for q in self.raw_passed_questions or []])
|
1589
1141
|
# question_names_string = ", ".join([repr(name) for name in self.question_names])
|
1590
|
-
return f"Survey(questions=[{questions_string}], memory_plan={self.memory_plan}, rule_collection={self.rule_collection}, question_groups={self.question_groups})"
|
1142
|
+
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})"
|
1591
1143
|
|
1592
1144
|
def _summary(self) -> dict:
|
1593
1145
|
return {
|
1594
|
-
"
|
1595
|
-
"
|
1596
|
-
"Question Names": self.question_names,
|
1146
|
+
"# questions": len(self),
|
1147
|
+
"question_name list": self.question_names,
|
1597
1148
|
}
|
1598
1149
|
|
1599
|
-
def _repr_html_(self) -> str:
|
1600
|
-
footer = f"<a href={self.__documentation__}>(docs)</a>"
|
1601
|
-
return str(self.summary(format="html")) + footer
|
1602
|
-
|
1603
1150
|
def tree(self, node_list: Optional[List[str]] = None):
|
1604
1151
|
return self.to_scenario_list().tree(node_list=node_list)
|
1605
1152
|
|
1606
1153
|
def table(self, *fields, tablefmt=None) -> Table:
|
1607
1154
|
return self.to_scenario_list().to_dataset().table(*fields, tablefmt=tablefmt)
|
1608
1155
|
|
1609
|
-
def rich_print(self) -> Table:
|
1610
|
-
"""Print the survey in a rich format.
|
1611
|
-
|
1612
|
-
>>> t = Survey.example().rich_print()
|
1613
|
-
>>> print(t) # doctest: +SKIP
|
1614
|
-
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
1615
|
-
┃ Questions ┃
|
1616
|
-
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
|
1617
|
-
│ ┏━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┓ │
|
1618
|
-
│ ┃ Question Name ┃ Question Type ┃ Question Text ┃ Options ┃ │
|
1619
|
-
│ ┡━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━┩ │
|
1620
|
-
│ │ q0 │ multiple_choice │ Do you like school? │ yes, no │ │
|
1621
|
-
│ └───────────────┴─────────────────┴─────────────────────┴─────────┘ │
|
1622
|
-
│ ┏━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ │
|
1623
|
-
│ ┃ Question Name ┃ Question Type ┃ Question Text ┃ Options ┃ │
|
1624
|
-
│ ┡━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │
|
1625
|
-
│ │ q1 │ multiple_choice │ Why not? │ killer bees in cafeteria, other │ │
|
1626
|
-
│ └───────────────┴─────────────────┴───────────────┴─────────────────────────────────┘ │
|
1627
|
-
│ ┏━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ │
|
1628
|
-
│ ┃ Question Name ┃ Question Type ┃ Question Text ┃ Options ┃ │
|
1629
|
-
│ ┡━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │
|
1630
|
-
│ │ q2 │ multiple_choice │ Why? │ **lack*** of killer bees in cafeteria, other │ │
|
1631
|
-
│ └───────────────┴─────────────────┴───────────────┴──────────────────────────────────────────────┘ │
|
1632
|
-
└────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
1633
|
-
"""
|
1634
|
-
from rich.table import Table
|
1635
|
-
|
1636
|
-
table = Table(show_header=True, header_style="bold magenta")
|
1637
|
-
table.add_column("Questions", style="dim")
|
1638
|
-
|
1639
|
-
for question in self._questions:
|
1640
|
-
table.add_row(question.rich_print())
|
1641
|
-
|
1642
|
-
return table
|
1643
|
-
|
1644
1156
|
# endregion
|
1645
1157
|
|
1646
1158
|
def codebook(self) -> dict[str, str]:
|
@@ -1655,37 +1167,6 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1655
1167
|
codebook[question.question_name] = question.question_text
|
1656
1168
|
return codebook
|
1657
1169
|
|
1658
|
-
# region: Export methods
|
1659
|
-
def to_csv(self, filename: str = None):
|
1660
|
-
"""Export the survey to a CSV file.
|
1661
|
-
|
1662
|
-
:param filename: The name of the file to save the CSV to.
|
1663
|
-
|
1664
|
-
>>> s = Survey.example()
|
1665
|
-
>>> s.to_csv() # doctest: +SKIP
|
1666
|
-
index question_name question_text question_options question_type
|
1667
|
-
0 0 q0 Do you like school? [yes, no] multiple_choice
|
1668
|
-
1 1 q1 Why not? [killer bees in cafeteria, other] multiple_choice
|
1669
|
-
2 2 q2 Why? [**lack*** of killer bees in cafeteria, other] multiple_choice
|
1670
|
-
"""
|
1671
|
-
raw_data = []
|
1672
|
-
for index, question in enumerate(self._questions):
|
1673
|
-
d = {"index": index}
|
1674
|
-
question_dict = question.to_dict()
|
1675
|
-
_ = question_dict.pop("edsl_version")
|
1676
|
-
_ = question_dict.pop("edsl_class_name")
|
1677
|
-
d.update(question_dict)
|
1678
|
-
raw_data.append(d)
|
1679
|
-
from pandas import DataFrame
|
1680
|
-
|
1681
|
-
df = DataFrame(raw_data)
|
1682
|
-
if filename:
|
1683
|
-
df.to_csv(filename, index=False)
|
1684
|
-
else:
|
1685
|
-
return df
|
1686
|
-
|
1687
|
-
# endregion
|
1688
|
-
|
1689
1170
|
@classmethod
|
1690
1171
|
def example(
|
1691
1172
|
cls,
|
@@ -1744,7 +1225,7 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1744
1225
|
|
1745
1226
|
def get_job(self, model=None, agent=None, **kwargs):
|
1746
1227
|
if model is None:
|
1747
|
-
from edsl import Model
|
1228
|
+
from edsl.language_models.model import Model
|
1748
1229
|
|
1749
1230
|
model = Model()
|
1750
1231
|
|
@@ -1753,7 +1234,7 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1753
1234
|
s = Scenario(kwargs)
|
1754
1235
|
|
1755
1236
|
if not agent:
|
1756
|
-
from edsl import Agent
|
1237
|
+
from edsl.agents.Agent import Agent
|
1757
1238
|
|
1758
1239
|
agent = Agent()
|
1759
1240
|
|
@@ -1765,26 +1246,24 @@ def main():
|
|
1765
1246
|
|
1766
1247
|
def example_survey():
|
1767
1248
|
"""Return an example survey."""
|
1768
|
-
from edsl
|
1769
|
-
from edsl.surveys.Survey import Survey
|
1249
|
+
from edsl import QuestionMultipleChoice, QuestionList, QuestionNumerical, Survey
|
1770
1250
|
|
1771
1251
|
q0 = QuestionMultipleChoice(
|
1772
|
-
question_text="Do you like school?",
|
1773
|
-
question_options=["yes", "no"],
|
1774
1252
|
question_name="q0",
|
1253
|
+
question_text="What is the capital of France?",
|
1254
|
+
question_options=["London", "Paris", "Rome", "Boston", "I don't know"]
|
1775
1255
|
)
|
1776
|
-
q1 =
|
1777
|
-
question_text="Why not?",
|
1778
|
-
question_options=["killer bees in cafeteria", "other"],
|
1256
|
+
q1 = QuestionList(
|
1779
1257
|
question_name="q1",
|
1258
|
+
question_text="Name some cities in France.",
|
1259
|
+
max_list_items = 5
|
1780
1260
|
)
|
1781
|
-
q2 =
|
1782
|
-
question_text="Why?",
|
1783
|
-
question_options=["**lack*** of killer bees in cafeteria", "other"],
|
1261
|
+
q2 = QuestionNumerical(
|
1784
1262
|
question_name="q2",
|
1263
|
+
question_text="What is the population of {{ q0.answer }}?"
|
1785
1264
|
)
|
1786
1265
|
s = Survey(questions=[q0, q1, q2])
|
1787
|
-
s = s.add_rule(q0, "q0 == '
|
1266
|
+
s = s.add_rule(q0, "q0 == 'Paris'", q2)
|
1788
1267
|
return s
|
1789
1268
|
|
1790
1269
|
s = example_survey()
|