edsl 0.1.33.dev1__py3-none-any.whl → 0.1.33.dev2__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/TemplateLoader.py +24 -0
- edsl/__init__.py +8 -4
- edsl/agents/Agent.py +46 -14
- edsl/agents/AgentList.py +43 -0
- edsl/agents/Invigilator.py +125 -212
- edsl/agents/InvigilatorBase.py +140 -32
- edsl/agents/PromptConstructionMixin.py +43 -66
- edsl/agents/__init__.py +1 -0
- edsl/auto/AutoStudy.py +117 -0
- edsl/auto/StageBase.py +230 -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 +73 -0
- edsl/auto/SurveyCreatorPipeline.py +21 -0
- edsl/auto/utilities.py +224 -0
- edsl/config.py +38 -39
- edsl/coop/PriceFetcher.py +58 -0
- edsl/coop/coop.py +39 -5
- edsl/data/Cache.py +35 -1
- edsl/data_transfer_models.py +120 -38
- edsl/enums.py +2 -0
- edsl/exceptions/language_models.py +25 -1
- edsl/exceptions/questions.py +62 -5
- edsl/exceptions/results.py +4 -0
- edsl/inference_services/AnthropicService.py +13 -11
- edsl/inference_services/AwsBedrock.py +19 -17
- edsl/inference_services/AzureAI.py +37 -20
- edsl/inference_services/GoogleService.py +16 -12
- edsl/inference_services/GroqService.py +2 -0
- edsl/inference_services/InferenceServiceABC.py +24 -0
- edsl/inference_services/MistralAIService.py +120 -0
- edsl/inference_services/OpenAIService.py +41 -50
- edsl/inference_services/TestService.py +71 -0
- edsl/inference_services/models_available_cache.py +0 -6
- edsl/inference_services/registry.py +4 -0
- edsl/jobs/Answers.py +10 -12
- edsl/jobs/FailedQuestion.py +78 -0
- edsl/jobs/Jobs.py +18 -13
- edsl/jobs/buckets/TokenBucket.py +39 -14
- edsl/jobs/interviews/Interview.py +297 -77
- edsl/jobs/interviews/InterviewExceptionEntry.py +83 -19
- edsl/jobs/interviews/interview_exception_tracking.py +0 -70
- edsl/jobs/interviews/retry_management.py +3 -1
- edsl/jobs/runners/JobsRunnerAsyncio.py +116 -70
- edsl/jobs/runners/JobsRunnerStatusMixin.py +1 -1
- edsl/jobs/tasks/QuestionTaskCreator.py +30 -23
- edsl/jobs/tasks/TaskHistory.py +131 -213
- edsl/language_models/LanguageModel.py +239 -129
- edsl/language_models/ModelList.py +2 -2
- edsl/language_models/RegisterLanguageModelsMeta.py +14 -29
- edsl/language_models/fake_openai_call.py +15 -0
- edsl/language_models/fake_openai_service.py +61 -0
- edsl/language_models/registry.py +15 -2
- edsl/language_models/repair.py +0 -19
- edsl/language_models/utilities.py +61 -0
- edsl/prompts/Prompt.py +52 -2
- edsl/questions/AnswerValidatorMixin.py +23 -26
- edsl/questions/QuestionBase.py +273 -242
- edsl/questions/QuestionBaseGenMixin.py +133 -0
- edsl/questions/QuestionBasePromptsMixin.py +266 -0
- edsl/questions/QuestionBudget.py +6 -0
- edsl/questions/QuestionCheckBox.py +227 -35
- edsl/questions/QuestionExtract.py +98 -27
- edsl/questions/QuestionFreeText.py +46 -29
- edsl/questions/QuestionFunctional.py +7 -0
- edsl/questions/QuestionList.py +141 -22
- edsl/questions/QuestionMultipleChoice.py +173 -64
- edsl/questions/QuestionNumerical.py +87 -46
- edsl/questions/QuestionRank.py +182 -24
- edsl/questions/RegisterQuestionsMeta.py +31 -12
- edsl/questions/ResponseValidatorABC.py +169 -0
- edsl/questions/__init__.py +3 -4
- edsl/questions/decorators.py +21 -0
- edsl/questions/derived/QuestionLikertFive.py +10 -5
- edsl/questions/derived/QuestionLinearScale.py +11 -1
- edsl/questions/derived/QuestionTopK.py +6 -0
- edsl/questions/derived/QuestionYesNo.py +16 -1
- edsl/questions/descriptors.py +43 -7
- 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_registry.py +6 -2
- edsl/questions/templates/__init__.py +0 -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/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/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 +8 -0
- edsl/questions/templates/numerical/question_presentation.jinja +7 -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/Dataset.py +20 -0
- edsl/results/DatasetExportMixin.py +41 -47
- edsl/results/DatasetTree.py +145 -0
- edsl/results/Result.py +32 -5
- edsl/results/Results.py +131 -45
- edsl/results/ResultsDBMixin.py +3 -3
- edsl/results/Selector.py +118 -0
- edsl/results/tree_explore.py +115 -0
- edsl/scenarios/Scenario.py +10 -4
- edsl/scenarios/ScenarioList.py +348 -39
- edsl/scenarios/ScenarioListExportMixin.py +9 -0
- edsl/study/SnapShot.py +8 -1
- edsl/surveys/RuleCollection.py +2 -2
- edsl/surveys/Survey.py +634 -315
- edsl/surveys/SurveyExportMixin.py +71 -9
- edsl/surveys/SurveyFlowVisualizationMixin.py +2 -1
- edsl/surveys/SurveyQualtricsImport.py +75 -4
- edsl/surveys/instructions/ChangeInstruction.py +47 -0
- edsl/surveys/instructions/Instruction.py +34 -0
- edsl/surveys/instructions/InstructionCollection.py +77 -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 +111 -0
- edsl/templates/error_reporting/interviews.html +10 -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-0.1.33.dev1.dist-info → edsl-0.1.33.dev2.dist-info}/METADATA +4 -2
- edsl-0.1.33.dev2.dist-info/RECORD +289 -0
- edsl/jobs/interviews/InterviewTaskBuildingMixin.py +0 -286
- edsl/utilities/gcp_bucket/simple_example.py +0 -9
- edsl-0.1.33.dev1.dist-info/RECORD +0 -209
- {edsl-0.1.33.dev1.dist-info → edsl-0.1.33.dev2.dist-info}/LICENSE +0 -0
- {edsl-0.1.33.dev1.dist-info → edsl-0.1.33.dev2.dist-info}/WHEEL +0 -0
edsl/surveys/Survey.py
CHANGED
@@ -20,6 +20,24 @@ from edsl.surveys.SurveyExportMixin import SurveyExportMixin
|
|
20
20
|
from edsl.surveys.SurveyFlowVisualizationMixin import SurveyFlowVisualizationMixin
|
21
21
|
from edsl.utilities.decorators import add_edsl_version, remove_edsl_version
|
22
22
|
|
23
|
+
from edsl.agents.Agent import Agent
|
24
|
+
|
25
|
+
|
26
|
+
class ValidatedString(str):
|
27
|
+
def __new__(cls, content):
|
28
|
+
if "<>" in content:
|
29
|
+
raise ValueError(
|
30
|
+
"The expression contains '<>', which is not allowed. You probably mean '!='."
|
31
|
+
)
|
32
|
+
return super().__new__(cls, content)
|
33
|
+
|
34
|
+
|
35
|
+
# from edsl.surveys.Instruction import Instruction
|
36
|
+
# from edsl.surveys.Instruction import ChangeInstruction
|
37
|
+
from edsl.surveys.instructions.InstructionCollection import InstructionCollection
|
38
|
+
from edsl.surveys.instructions.Instruction import Instruction
|
39
|
+
from edsl.surveys.instructions.ChangeInstruction import ChangeInstruction
|
40
|
+
|
23
41
|
|
24
42
|
class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
25
43
|
"""A collection of questions that supports skip logic."""
|
@@ -42,11 +60,13 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
42
60
|
|
43
61
|
def __init__(
|
44
62
|
self,
|
45
|
-
questions: Optional[
|
63
|
+
questions: Optional[
|
64
|
+
list[Union[QuestionBase, Instruction, ChangeInstruction]]
|
65
|
+
] = None,
|
46
66
|
memory_plan: Optional[MemoryPlan] = None,
|
47
67
|
rule_collection: Optional[RuleCollection] = None,
|
48
68
|
question_groups: Optional[dict[str, tuple[int, int]]] = None,
|
49
|
-
name: str = None,
|
69
|
+
name: Optional[str] = None,
|
50
70
|
):
|
51
71
|
"""Create a new survey.
|
52
72
|
|
@@ -56,13 +76,33 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
56
76
|
:param question_groups: The groups of questions in the survey.
|
57
77
|
:param name: The name of the survey - DEPRECATED.
|
58
78
|
|
79
|
+
|
80
|
+
>>> from edsl import QuestionFreeText
|
81
|
+
>>> q1 = QuestionFreeText(question_text = "What is your name?", question_name = "name")
|
82
|
+
>>> q2 = QuestionFreeText(question_text = "What is your favorite color?", question_name = "color")
|
83
|
+
>>> q3 = QuestionFreeText(question_text = "Is a hot dog a sandwich", question_name = "food")
|
84
|
+
>>> s = Survey([q1, q2, q3], question_groups = {"demographics": (0, 1), "substantive":(3)})
|
85
|
+
|
86
|
+
|
59
87
|
"""
|
88
|
+
|
89
|
+
self.raw_passed_questions = questions
|
90
|
+
|
91
|
+
(
|
92
|
+
true_questions,
|
93
|
+
instruction_names_to_instructions,
|
94
|
+
self.pseudo_indices,
|
95
|
+
) = self._separate_questions_and_instructions(questions or [])
|
96
|
+
|
60
97
|
self.rule_collection = RuleCollection(
|
61
|
-
num_questions=len(
|
98
|
+
num_questions=len(true_questions) if true_questions else None
|
62
99
|
)
|
63
100
|
# the RuleCollection needs to be present while we add the questions; we might override this later
|
64
101
|
# if a rule_collection is provided. This allows us to serialize the survey with the rule_collection.
|
65
|
-
|
102
|
+
|
103
|
+
self.questions = true_questions
|
104
|
+
self.instruction_names_to_instructions = instruction_names_to_instructions
|
105
|
+
|
66
106
|
self.memory_plan = memory_plan or MemoryPlan(self)
|
67
107
|
if question_groups is not None:
|
68
108
|
self.question_groups = question_groups
|
@@ -78,6 +118,177 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
78
118
|
|
79
119
|
warnings.warn("name parameter to a survey is deprecated.")
|
80
120
|
|
121
|
+
# region: Suvry instruction handling
|
122
|
+
@property
|
123
|
+
def relevant_instructions_dict(self) -> InstructionCollection:
|
124
|
+
"""Return a dictionary with keys as question names and values as instructions that are relevant to the question.
|
125
|
+
|
126
|
+
>>> s = Survey.example(include_instructions=True)
|
127
|
+
>>> s.relevant_instructions_dict
|
128
|
+
{'q0': [Instruction(name="attention", text="Please pay attention!")], 'q1': [Instruction(name="attention", text="Please pay attention!")], 'q2': [Instruction(name="attention", text="Please pay attention!")]}
|
129
|
+
|
130
|
+
"""
|
131
|
+
return InstructionCollection(
|
132
|
+
self.instruction_names_to_instructions, self.questions
|
133
|
+
)
|
134
|
+
|
135
|
+
@staticmethod
|
136
|
+
def _separate_questions_and_instructions(questions_and_instructions: list) -> tuple:
|
137
|
+
"""
|
138
|
+
The 'pseudo_indices' attribute is a dictionary that maps question names to pseudo-indices
|
139
|
+
that are used to order questions and instructions in the survey.
|
140
|
+
Only questions get real indices; instructions get pseudo-indices.
|
141
|
+
However, the order of the pseudo-indices is the same as the order questions and instructions are added to the survey.
|
142
|
+
|
143
|
+
We don't have to know how many instructions there are to calculate the pseudo-indices because they are
|
144
|
+
calculated by the inverse of one minus the sum of 1/2^n for n in the number of instructions run so far.
|
145
|
+
|
146
|
+
>>> from edsl import Instruction
|
147
|
+
>>> i = Instruction(text = "Pay attention to the following questions.", name = "intro")
|
148
|
+
>>> i2 = Instruction(text = "How are you feeling today?", name = "followon_intro")
|
149
|
+
>>> from edsl import QuestionFreeText; q1 = QuestionFreeText.example()
|
150
|
+
>>> from edsl import QuestionMultipleChoice; q2 = QuestionMultipleChoice.example()
|
151
|
+
>>> s = Survey([q1, i, i2, q2])
|
152
|
+
>>> len(s.instruction_names_to_instructions)
|
153
|
+
2
|
154
|
+
>>> s.pseudo_indices
|
155
|
+
{'how_are_you': 0, 'intro': 0.5, 'followon_intro': 0.75, 'how_feeling': 1}
|
156
|
+
|
157
|
+
>>> from edsl import ChangeInstruction
|
158
|
+
>>> q3 = QuestionFreeText(question_text = "What is your favorite color?", question_name = "color")
|
159
|
+
>>> i_change = ChangeInstruction(drop = ["intro"])
|
160
|
+
>>> s = Survey([q1, i, q2, i_change, q3])
|
161
|
+
>>> [i.name for i in s.relevant_instructions(q1)]
|
162
|
+
[]
|
163
|
+
>>> [i.name for i in s.relevant_instructions(q2)]
|
164
|
+
['intro']
|
165
|
+
>>> [i.name for i in s.relevant_instructions(q3)]
|
166
|
+
[]
|
167
|
+
|
168
|
+
>>> i_change = ChangeInstruction(keep = ["poop"], drop = [])
|
169
|
+
>>> s = Survey([q1, i, q2, i_change])
|
170
|
+
Traceback (most recent call last):
|
171
|
+
...
|
172
|
+
ValueError: ChangeInstruction change_instruction_0 references instruction poop which does not exist.
|
173
|
+
"""
|
174
|
+
from edsl.surveys.instructions.Instruction import Instruction
|
175
|
+
from edsl.surveys.instructions.ChangeInstruction import ChangeInstruction
|
176
|
+
|
177
|
+
true_questions = []
|
178
|
+
instruction_names_to_instructions = {}
|
179
|
+
|
180
|
+
num_change_instructions = 0
|
181
|
+
pseudo_indices = {}
|
182
|
+
instructions_run_length = 0
|
183
|
+
for entry in questions_and_instructions:
|
184
|
+
if isinstance(entry, Instruction) or isinstance(entry, ChangeInstruction):
|
185
|
+
if isinstance(entry, ChangeInstruction):
|
186
|
+
entry.add_name(num_change_instructions)
|
187
|
+
num_change_instructions += 1
|
188
|
+
for prior_instruction in entry.keep + entry.drop:
|
189
|
+
if prior_instruction not in instruction_names_to_instructions:
|
190
|
+
raise ValueError(
|
191
|
+
f"ChangeInstruction {entry.name} references instruction {prior_instruction} which does not exist."
|
192
|
+
)
|
193
|
+
instructions_run_length += 1
|
194
|
+
delta = 1 - 1.0 / (2.0**instructions_run_length)
|
195
|
+
pseudo_index = (len(true_questions) - 1) + delta
|
196
|
+
entry.pseudo_index = pseudo_index
|
197
|
+
instruction_names_to_instructions[entry.name] = entry
|
198
|
+
elif isinstance(entry, QuestionBase):
|
199
|
+
pseudo_index = len(true_questions)
|
200
|
+
instructions_run_length = 0
|
201
|
+
true_questions.append(entry)
|
202
|
+
else:
|
203
|
+
raise ValueError(
|
204
|
+
f"Entry {repr(entry)} is not a QuestionBase or an Instruction."
|
205
|
+
)
|
206
|
+
|
207
|
+
pseudo_indices[entry.name] = pseudo_index
|
208
|
+
|
209
|
+
return true_questions, instruction_names_to_instructions, pseudo_indices
|
210
|
+
|
211
|
+
def relevant_instructions(self, question) -> dict:
|
212
|
+
"""This should be a dictionry with keys as question names and values as instructions that are relevant to the question.
|
213
|
+
|
214
|
+
:param question: The question to get the relevant instructions for.
|
215
|
+
|
216
|
+
# Did the instruction come before the question and was it not modified by a change instruction?
|
217
|
+
|
218
|
+
"""
|
219
|
+
return self.relevant_instructions_dict[question]
|
220
|
+
|
221
|
+
@property
|
222
|
+
def max_pseudo_index(self) -> float:
|
223
|
+
"""Return the maximum pseudo index in the survey.
|
224
|
+
|
225
|
+
Example:
|
226
|
+
|
227
|
+
>>> s = Survey.example()
|
228
|
+
>>> s.max_pseudo_index
|
229
|
+
2
|
230
|
+
"""
|
231
|
+
if len(self.pseudo_indices) == 0:
|
232
|
+
return -1
|
233
|
+
return max(self.pseudo_indices.values())
|
234
|
+
|
235
|
+
@property
|
236
|
+
def last_item_was_instruction(self) -> bool:
|
237
|
+
"""Return whether the last item added to the survey was an instruction.
|
238
|
+
This is used to determine the pseudo-index of the next item added to the survey.
|
239
|
+
|
240
|
+
Example:
|
241
|
+
|
242
|
+
>>> s = Survey.example()
|
243
|
+
>>> s.last_item_was_instruction
|
244
|
+
False
|
245
|
+
>>> from edsl.surveys.instructions.Instruction import Instruction
|
246
|
+
>>> s = s.add_instruction(Instruction(text="Pay attention to the following questions.", name="intro"))
|
247
|
+
>>> s.last_item_was_instruction
|
248
|
+
True
|
249
|
+
"""
|
250
|
+
return isinstance(self.max_pseudo_index, float)
|
251
|
+
|
252
|
+
def add_instruction(
|
253
|
+
self, instruction: Union["Instruction", "ChangeInstruction"]
|
254
|
+
) -> Survey:
|
255
|
+
"""
|
256
|
+
Add an instruction to the survey.
|
257
|
+
|
258
|
+
:param instruction: The instruction to add to the survey.
|
259
|
+
|
260
|
+
>>> from edsl import Instruction
|
261
|
+
>>> i = Instruction(text="Pay attention to the following questions.", name="intro")
|
262
|
+
>>> s = Survey().add_instruction(i)
|
263
|
+
>>> s.instruction_names_to_instructions
|
264
|
+
{'intro': Instruction(name="intro", text="Pay attention to the following questions.")}
|
265
|
+
>>> s.pseudo_indices
|
266
|
+
{'intro': -0.5}
|
267
|
+
"""
|
268
|
+
import math
|
269
|
+
|
270
|
+
if instruction.name in self.instruction_names_to_instructions:
|
271
|
+
raise SurveyCreationError(
|
272
|
+
f"""Instruction name '{instruction.name}' already exists in survey. Existing names are {self.instruction_names_to_instructions.keys()}."""
|
273
|
+
)
|
274
|
+
self.instruction_names_to_instructions[instruction.name] = instruction
|
275
|
+
|
276
|
+
# was the last thing added an instruction or a question?
|
277
|
+
if self.last_item_was_instruction:
|
278
|
+
pseudo_index = (
|
279
|
+
self.max_pseudo_index
|
280
|
+
+ (math.ceil(self.max_pseudo_index) - self.max_pseudo_index) / 2
|
281
|
+
)
|
282
|
+
else:
|
283
|
+
pseudo_index = self.max_pseudo_index + 1.0 / 2.0
|
284
|
+
self.pseudo_indices[instruction.name] = pseudo_index
|
285
|
+
|
286
|
+
return self
|
287
|
+
|
288
|
+
# endregion
|
289
|
+
|
290
|
+
# region: Simulation methods
|
291
|
+
|
81
292
|
def simulate(self) -> dict:
|
82
293
|
"""Simulate the survey and return the answers."""
|
83
294
|
i = self.gen_path_through_survey()
|
@@ -93,9 +304,6 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
93
304
|
def create_agent(self) -> "Agent":
|
94
305
|
"""Create an agent from the simulated answers."""
|
95
306
|
answers_dict = self.simulate()
|
96
|
-
from edsl.agents.Agent import Agent
|
97
|
-
|
98
|
-
a = Agent(traits=answers_dict)
|
99
307
|
|
100
308
|
def construct_answer_dict_function(traits: dict) -> Callable:
|
101
309
|
def func(self, question: "QuestionBase", scenario=None):
|
@@ -103,16 +311,48 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
103
311
|
|
104
312
|
return func
|
105
313
|
|
106
|
-
|
314
|
+
return Agent(traits=answers_dict).add_direct_question_answering_method(
|
107
315
|
construct_answer_dict_function(answers_dict)
|
108
316
|
)
|
109
|
-
return a
|
110
317
|
|
111
318
|
def simulate_results(self) -> "Results":
|
112
319
|
"""Simulate the survey and return the results."""
|
113
320
|
a = self.create_agent()
|
114
321
|
return self.by([a]).run()
|
115
322
|
|
323
|
+
# endregion
|
324
|
+
|
325
|
+
# region: Access methods
|
326
|
+
def _get_question_index(
|
327
|
+
self, q: Union[QuestionBase, str, EndOfSurvey.__class__]
|
328
|
+
) -> Union[int, EndOfSurvey.__class__]:
|
329
|
+
"""Return the index of the question or EndOfSurvey object.
|
330
|
+
|
331
|
+
:param q: The question or question name to get the index of.
|
332
|
+
|
333
|
+
It can handle it if the user passes in the question name, the question object, or the EndOfSurvey object.
|
334
|
+
|
335
|
+
>>> s = Survey.example()
|
336
|
+
>>> s._get_question_index("q0")
|
337
|
+
0
|
338
|
+
|
339
|
+
This doesnt' work with questions that don't exist:
|
340
|
+
|
341
|
+
>>> s._get_question_index("poop")
|
342
|
+
Traceback (most recent call last):
|
343
|
+
...
|
344
|
+
ValueError: Question name poop not found in survey. The current question names are {'q0': 0, 'q1': 1, 'q2': 2}.
|
345
|
+
"""
|
346
|
+
if q == EndOfSurvey:
|
347
|
+
return EndOfSurvey
|
348
|
+
else:
|
349
|
+
question_name = q if isinstance(q, str) else q.question_name
|
350
|
+
if question_name not in self.question_name_to_index:
|
351
|
+
raise ValueError(
|
352
|
+
f"""Question name {question_name} not found in survey. The current question names are {self.question_name_to_index}."""
|
353
|
+
)
|
354
|
+
return self.question_name_to_index[question_name]
|
355
|
+
|
116
356
|
def get(self, question_name: str) -> QuestionBase:
|
117
357
|
"""
|
118
358
|
Return the question object given the question name.
|
@@ -128,54 +368,157 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
128
368
|
index = self.question_name_to_index[question_name]
|
129
369
|
return self._questions[index]
|
130
370
|
|
131
|
-
def question_names_to_questions(self) -> dict:
|
132
|
-
"""Return a dictionary mapping question names to question attributes."""
|
133
|
-
return {q.question_name: q for q in self.questions}
|
134
|
-
|
135
371
|
def get_question(self, question_name: str) -> QuestionBase:
|
136
372
|
"""Return the question object given the question name."""
|
137
373
|
# import warnings
|
138
374
|
# warnings.warn("survey.get_question is deprecated. Use subscript operator instead.")
|
139
375
|
return self.get(question_name)
|
140
376
|
|
377
|
+
def question_names_to_questions(self) -> dict:
|
378
|
+
"""Return a dictionary mapping question names to question attributes."""
|
379
|
+
return {q.question_name: q for q in self.questions}
|
380
|
+
|
381
|
+
@property
|
382
|
+
def question_names(self) -> list[str]:
|
383
|
+
"""Return a list of question names in the survey.
|
384
|
+
|
385
|
+
Example:
|
386
|
+
|
387
|
+
>>> s = Survey.example()
|
388
|
+
>>> s.question_names
|
389
|
+
['q0', 'q1', 'q2']
|
390
|
+
"""
|
391
|
+
# return list(self.question_name_to_index.keys())
|
392
|
+
return [q.question_name for q in self.questions]
|
393
|
+
|
394
|
+
@property
|
395
|
+
def question_name_to_index(self) -> dict[str, int]:
|
396
|
+
"""Return a dictionary mapping question names to question indices.
|
397
|
+
|
398
|
+
Example:
|
399
|
+
|
400
|
+
>>> s = Survey.example()
|
401
|
+
>>> s.question_name_to_index
|
402
|
+
{'q0': 0, 'q1': 1, 'q2': 2}
|
403
|
+
"""
|
404
|
+
return {q.question_name: i for i, q in enumerate(self.questions)}
|
405
|
+
|
406
|
+
# endregion
|
407
|
+
|
408
|
+
# region: serialization methods
|
141
409
|
def __hash__(self) -> int:
|
142
410
|
"""Return a hash of the question."""
|
143
411
|
from edsl.utilities.utilities import dict_hash
|
144
412
|
|
145
413
|
return dict_hash(self._to_dict())
|
146
414
|
|
147
|
-
def
|
148
|
-
"""
|
415
|
+
def _to_dict(self) -> dict[str, Any]:
|
416
|
+
"""Serialize the Survey object to a dictionary.
|
149
417
|
|
150
|
-
|
151
|
-
>>>
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
418
|
+
>>> s = Survey.example()
|
419
|
+
>>> s._to_dict().keys()
|
420
|
+
dict_keys(['questions', 'memory_plan', 'rule_collection', 'question_groups'])
|
421
|
+
"""
|
422
|
+
return {
|
423
|
+
"questions": [
|
424
|
+
q._to_dict() for q in self.recombined_questions_and_instructions()
|
425
|
+
],
|
426
|
+
"memory_plan": self.memory_plan.to_dict(),
|
427
|
+
"rule_collection": self.rule_collection.to_dict(),
|
428
|
+
"question_groups": self.question_groups,
|
429
|
+
}
|
430
|
+
|
431
|
+
@add_edsl_version
|
432
|
+
def to_dict(self) -> dict[str, Any]:
|
433
|
+
"""Serialize the Survey object to a dictionary.
|
434
|
+
|
435
|
+
>>> s = Survey.example()
|
436
|
+
>>> s.to_dict().keys()
|
437
|
+
dict_keys(['questions', 'memory_plan', 'rule_collection', 'question_groups', 'edsl_version', 'edsl_class_name'])
|
161
438
|
|
162
439
|
"""
|
163
|
-
|
164
|
-
len(self.rule_collection.non_default_rules) > 0
|
165
|
-
or len(other.rule_collection.non_default_rules) > 0
|
166
|
-
):
|
167
|
-
raise ValueError(
|
168
|
-
"Cannot combine two surveys with non-default rules.",
|
169
|
-
"Please use the 'clear_non_default_rules' method to remove non-default rules from the survey.",
|
170
|
-
)
|
440
|
+
return self._to_dict()
|
171
441
|
|
172
|
-
|
442
|
+
@classmethod
|
443
|
+
@remove_edsl_version
|
444
|
+
def from_dict(cls, data: dict) -> Survey:
|
445
|
+
"""Deserialize the dictionary back to a Survey object.
|
173
446
|
|
174
|
-
|
175
|
-
|
447
|
+
:param data: The dictionary to deserialize.
|
448
|
+
|
449
|
+
>>> d = Survey.example().to_dict()
|
450
|
+
>>> s = Survey.from_dict(d)
|
451
|
+
>>> s == Survey.example()
|
452
|
+
True
|
453
|
+
|
454
|
+
>>> s = Survey.example(include_instructions = True)
|
455
|
+
>>> d = s.to_dict()
|
456
|
+
>>> news = Survey.from_dict(d)
|
457
|
+
>>> news == s
|
458
|
+
True
|
459
|
+
|
460
|
+
"""
|
461
|
+
|
462
|
+
def get_class(pass_dict):
|
463
|
+
if (class_name := pass_dict.get("edsl_class_name")) == "QuestionBase":
|
464
|
+
return QuestionBase
|
465
|
+
elif class_name == "Instruction":
|
466
|
+
from edsl.surveys.instructions.Instruction import Instruction
|
467
|
+
|
468
|
+
return Instruction
|
469
|
+
elif class_name == "ChangeInstruction":
|
470
|
+
from edsl.surveys.instructions.ChangeInstruction import (
|
471
|
+
ChangeInstruction,
|
472
|
+
)
|
473
|
+
|
474
|
+
return ChangeInstruction
|
475
|
+
else:
|
476
|
+
# some data might not have the edsl_class_name
|
477
|
+
return QuestionBase
|
478
|
+
# raise ValueError(f"Class {pass_dict['edsl_class_name']} not found")
|
479
|
+
|
480
|
+
questions = [
|
481
|
+
get_class(q_dict).from_dict(q_dict) for q_dict in data["questions"]
|
482
|
+
]
|
483
|
+
memory_plan = MemoryPlan.from_dict(data["memory_plan"])
|
484
|
+
survey = cls(
|
485
|
+
questions=questions,
|
486
|
+
memory_plan=memory_plan,
|
487
|
+
rule_collection=RuleCollection.from_dict(data["rule_collection"]),
|
488
|
+
question_groups=data["question_groups"],
|
489
|
+
)
|
490
|
+
return survey
|
491
|
+
|
492
|
+
# endregion
|
493
|
+
|
494
|
+
# region: Survey template parameters
|
495
|
+
@property
|
496
|
+
def scenario_attributes(self) -> list[str]:
|
497
|
+
"""Return a list of attributes that admissible Scenarios should have.
|
498
|
+
|
499
|
+
Here we have a survey with a question that uses a jinja2 style {{ }} template:
|
500
|
+
|
501
|
+
>>> from edsl import QuestionFreeText
|
502
|
+
>>> s = Survey().add_question(QuestionFreeText(question_text="{{ greeting }}. What is your name?", question_name="name"))
|
503
|
+
>>> s.scenario_attributes
|
504
|
+
['greeting']
|
505
|
+
|
506
|
+
>>> s = Survey().add_question(QuestionFreeText(question_text="{{ greeting }}. What is your {{ attribute }}?", question_name="name"))
|
507
|
+
>>> s.scenario_attributes
|
508
|
+
['greeting', 'attribute']
|
509
|
+
|
510
|
+
|
511
|
+
"""
|
512
|
+
temp = []
|
176
513
|
for question in self.questions:
|
177
|
-
|
178
|
-
|
514
|
+
question_text = question.question_text
|
515
|
+
# extract the contents of all {{ }} in the question text using regex
|
516
|
+
matches = re.findall(r"\{\{(.+?)\}\}", question_text)
|
517
|
+
# remove whitespace
|
518
|
+
matches = [match.strip() for match in matches]
|
519
|
+
# add them to the temp list
|
520
|
+
temp.extend(matches)
|
521
|
+
return temp
|
179
522
|
|
180
523
|
@property
|
181
524
|
def parameters(self):
|
@@ -189,32 +532,46 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
189
532
|
|
190
533
|
@property
|
191
534
|
def parameters_by_question(self):
|
535
|
+
"""Return a dictionary of parameters by question in the survey.
|
536
|
+
>>> from edsl import QuestionFreeText
|
537
|
+
>>> q = QuestionFreeText(question_name = "example", question_text = "What is the capital of {{ country}}?")
|
538
|
+
>>> s = Survey([q])
|
539
|
+
>>> s.parameters_by_question
|
540
|
+
{'example': {'country'}}
|
541
|
+
"""
|
192
542
|
return {q.question_name: q.parameters for q in self.questions}
|
193
543
|
|
194
|
-
|
195
|
-
def question_names(self) -> list[str]:
|
196
|
-
"""Return a list of question names in the survey.
|
544
|
+
# endregion
|
197
545
|
|
198
|
-
|
199
|
-
|
200
|
-
>>> s = Survey.example()
|
201
|
-
>>> s.question_names
|
202
|
-
['q0', 'q1', 'q2']
|
203
|
-
"""
|
204
|
-
# return list(self.question_name_to_index.keys())
|
205
|
-
return [q.question_name for q in self.questions]
|
546
|
+
# region: Survey construction
|
206
547
|
|
207
|
-
|
208
|
-
def
|
209
|
-
"""
|
548
|
+
# region: Adding questions and combining surveys
|
549
|
+
def __add__(self, other: Survey) -> Survey:
|
550
|
+
"""Combine two surveys.
|
210
551
|
|
211
|
-
|
552
|
+
:param other: The other survey to combine with this one.
|
553
|
+
>>> s1 = Survey.example()
|
554
|
+
>>> from edsl import QuestionFreeText
|
555
|
+
>>> s2 = Survey([QuestionFreeText(question_text="What is your name?", question_name="yo")])
|
556
|
+
>>> s3 = s1 + s2
|
557
|
+
Traceback (most recent call last):
|
558
|
+
...
|
559
|
+
ValueError: ('Cannot combine two surveys with non-default rules.', "Please use the 'clear_non_default_rules' method to remove non-default rules from the survey.")
|
560
|
+
>>> s3 = s1.clear_non_default_rules() + s2
|
561
|
+
>>> len(s3.questions)
|
562
|
+
4
|
212
563
|
|
213
|
-
>>> s = Survey.example()
|
214
|
-
>>> s.question_name_to_index
|
215
|
-
{'q0': 0, 'q1': 1, 'q2': 2}
|
216
564
|
"""
|
217
|
-
|
565
|
+
if (
|
566
|
+
len(self.rule_collection.non_default_rules) > 0
|
567
|
+
or len(other.rule_collection.non_default_rules) > 0
|
568
|
+
):
|
569
|
+
raise ValueError(
|
570
|
+
"Cannot combine two surveys with non-default rules.",
|
571
|
+
"Please use the 'clear_non_default_rules' method to remove non-default rules from the survey.",
|
572
|
+
)
|
573
|
+
|
574
|
+
return Survey(questions=self.questions + other.questions)
|
218
575
|
|
219
576
|
def add_question(self, question: QuestionBase) -> Survey:
|
220
577
|
"""
|
@@ -233,11 +590,11 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
233
590
|
>>> s = Survey().add_question(q).add_question(q)
|
234
591
|
Traceback (most recent call last):
|
235
592
|
...
|
236
|
-
edsl.exceptions.surveys.SurveyCreationError: Question name already exists in survey.
|
593
|
+
edsl.exceptions.surveys.SurveyCreationError: Question name 'q0' already exists in survey. Existing names are ['q0'].
|
237
594
|
"""
|
238
595
|
if question.question_name in self.question_names:
|
239
596
|
raise SurveyCreationError(
|
240
|
-
f"""Question name already exists in survey.
|
597
|
+
f"""Question name '{question.question_name}' already exists in survey. Existing names are {self.question_names}."""
|
241
598
|
)
|
242
599
|
index = len(self.questions)
|
243
600
|
# TODO: This is a bit ugly because the user
|
@@ -245,6 +602,8 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
245
602
|
# descriptor.
|
246
603
|
self._questions.append(question)
|
247
604
|
|
605
|
+
self.pseudo_indices[question.question_name] = index
|
606
|
+
|
248
607
|
# using index + 1 presumes there is a next question
|
249
608
|
self.rule_collection.add_rule(
|
250
609
|
Rule(
|
@@ -263,6 +622,20 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
263
622
|
|
264
623
|
return self
|
265
624
|
|
625
|
+
def recombined_questions_and_instructions(
|
626
|
+
self,
|
627
|
+
) -> list[Union[QuestionBase, "Instruction"]]:
|
628
|
+
"""Return a list of questions and instructions sorted by pseudo index."""
|
629
|
+
questions_and_instructions = self._questions + list(
|
630
|
+
self.instruction_names_to_instructions.values()
|
631
|
+
)
|
632
|
+
return sorted(
|
633
|
+
questions_and_instructions, key=lambda x: self.pseudo_indices[x.name]
|
634
|
+
)
|
635
|
+
|
636
|
+
# endregion
|
637
|
+
|
638
|
+
# region: Memory plan methods
|
266
639
|
def set_full_memory_mode(self) -> Survey:
|
267
640
|
"""Add instructions to a survey that the agent should remember all of the answers to the questions in the survey.
|
268
641
|
|
@@ -295,6 +668,75 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
295
668
|
prior_questions=prior_questions_func(i),
|
296
669
|
)
|
297
670
|
|
671
|
+
def add_targeted_memory(
|
672
|
+
self,
|
673
|
+
focal_question: Union[QuestionBase, str],
|
674
|
+
prior_question: Union[QuestionBase, str],
|
675
|
+
) -> Survey:
|
676
|
+
"""Add instructions to a survey than when answering focal_question.
|
677
|
+
|
678
|
+
:param focal_question: The question that the agent is answering.
|
679
|
+
:param prior_question: The question that the agent should remember when answering the focal question.
|
680
|
+
|
681
|
+
Here we add instructions to a survey than when answering q2 they should remember q1:
|
682
|
+
|
683
|
+
>>> s = Survey.example().add_targeted_memory("q2", "q0")
|
684
|
+
>>> s.memory_plan
|
685
|
+
{'q2': Memory(prior_questions=['q0'])}
|
686
|
+
|
687
|
+
The agent should also remember the answers to prior_questions listed in prior_questions.
|
688
|
+
"""
|
689
|
+
focal_question_name = self.question_names[
|
690
|
+
self._get_question_index(focal_question)
|
691
|
+
]
|
692
|
+
prior_question_name = self.question_names[
|
693
|
+
self._get_question_index(prior_question)
|
694
|
+
]
|
695
|
+
|
696
|
+
self.memory_plan.add_single_memory(
|
697
|
+
focal_question=focal_question_name,
|
698
|
+
prior_question=prior_question_name,
|
699
|
+
)
|
700
|
+
|
701
|
+
return self
|
702
|
+
|
703
|
+
def add_memory_collection(
|
704
|
+
self,
|
705
|
+
focal_question: Union[QuestionBase, str],
|
706
|
+
prior_questions: List[Union[QuestionBase, str]],
|
707
|
+
) -> Survey:
|
708
|
+
"""Add prior questions and responses so the agent has them when answering.
|
709
|
+
|
710
|
+
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.
|
711
|
+
|
712
|
+
:param focal_question: The question that the agent is answering.
|
713
|
+
:param prior_questions: The questions that the agent should remember when answering the focal question.
|
714
|
+
|
715
|
+
Here we have it so that when answering q2, the agent should remember answers to q0 and q1:
|
716
|
+
|
717
|
+
>>> s = Survey.example().add_memory_collection("q2", ["q0", "q1"])
|
718
|
+
>>> s.memory_plan
|
719
|
+
{'q2': Memory(prior_questions=['q0', 'q1'])}
|
720
|
+
"""
|
721
|
+
focal_question_name = self.question_names[
|
722
|
+
self._get_question_index(focal_question)
|
723
|
+
]
|
724
|
+
|
725
|
+
prior_question_names = [
|
726
|
+
self.question_names[self._get_question_index(prior_question)]
|
727
|
+
for prior_question in prior_questions
|
728
|
+
]
|
729
|
+
|
730
|
+
self.memory_plan.add_memory_collection(
|
731
|
+
focal_question=focal_question_name, prior_questions=prior_question_names
|
732
|
+
)
|
733
|
+
return self
|
734
|
+
|
735
|
+
# endregion
|
736
|
+
# endregion
|
737
|
+
# endregion
|
738
|
+
|
739
|
+
# region: Question groups
|
298
740
|
def add_question_group(
|
299
741
|
self,
|
300
742
|
start_question: Union[QuestionBase, str],
|
@@ -369,73 +811,28 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
369
811
|
if start_index < existing_end_index and end_index > existing_end_index:
|
370
812
|
raise ValueError(f"Group {group_name} overlaps with the new group.")
|
371
813
|
|
372
|
-
self.question_groups[group_name] = (start_index, end_index)
|
373
|
-
return self
|
374
|
-
|
375
|
-
def add_targeted_memory(
|
376
|
-
self,
|
377
|
-
focal_question: Union[QuestionBase, str],
|
378
|
-
prior_question: Union[QuestionBase, str],
|
379
|
-
) -> Survey:
|
380
|
-
"""Add instructions to a survey than when answering focal_question.
|
381
|
-
|
382
|
-
:param focal_question: The question that the agent is answering.
|
383
|
-
:param prior_question: The question that the agent should remember when answering the focal question.
|
384
|
-
|
385
|
-
Here we add instructions to a survey than when answering q2 they should remember q1:
|
386
|
-
|
387
|
-
>>> s = Survey.example().add_targeted_memory("q2", "q0")
|
388
|
-
>>> s.memory_plan
|
389
|
-
{'q2': Memory(prior_questions=['q0'])}
|
390
|
-
|
391
|
-
The agent should also remember the answers to prior_questions listed in prior_questions.
|
392
|
-
"""
|
393
|
-
focal_question_name = self.question_names[
|
394
|
-
self._get_question_index(focal_question)
|
395
|
-
]
|
396
|
-
prior_question_name = self.question_names[
|
397
|
-
self._get_question_index(prior_question)
|
398
|
-
]
|
399
|
-
|
400
|
-
self.memory_plan.add_single_memory(
|
401
|
-
focal_question=focal_question_name,
|
402
|
-
prior_question=prior_question_name,
|
403
|
-
)
|
404
|
-
|
405
|
-
return self
|
406
|
-
|
407
|
-
def add_memory_collection(
|
408
|
-
self,
|
409
|
-
focal_question: Union[QuestionBase, str],
|
410
|
-
prior_questions: List[Union[QuestionBase, str]],
|
411
|
-
) -> Survey:
|
412
|
-
"""Add prior questions and responses so the agent has them when answering.
|
413
|
-
|
414
|
-
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.
|
415
|
-
|
416
|
-
:param focal_question: The question that the agent is answering.
|
417
|
-
:param prior_questions: The questions that the agent should remember when answering the focal question.
|
418
|
-
|
419
|
-
Here we have it so that when answering q2, the agent should remember answers to q0 and q1:
|
420
|
-
|
421
|
-
>>> s = Survey.example().add_memory_collection("q2", ["q0", "q1"])
|
422
|
-
>>> s.memory_plan
|
423
|
-
{'q2': Memory(prior_questions=['q0', 'q1'])}
|
424
|
-
"""
|
425
|
-
focal_question_name = self.question_names[
|
426
|
-
self._get_question_index(focal_question)
|
427
|
-
]
|
428
|
-
|
429
|
-
prior_question_names = [
|
430
|
-
self.question_names[self._get_question_index(prior_question)]
|
431
|
-
for prior_question in prior_questions
|
432
|
-
]
|
433
|
-
|
434
|
-
self.memory_plan.add_memory_collection(
|
435
|
-
focal_question=focal_question_name, prior_questions=prior_question_names
|
436
|
-
)
|
814
|
+
self.question_groups[group_name] = (start_index, end_index)
|
437
815
|
return self
|
438
816
|
|
817
|
+
# endregion
|
818
|
+
|
819
|
+
# region: Survey rules
|
820
|
+
def show_rules(self) -> None:
|
821
|
+
"""Print out the rules in the survey.
|
822
|
+
|
823
|
+
>>> s = Survey.example()
|
824
|
+
>>> s.show_rules()
|
825
|
+
┏━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━┓
|
826
|
+
┃ current_q ┃ expression ┃ next_q ┃ priority ┃ before_rule ┃
|
827
|
+
┡━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━┩
|
828
|
+
│ 0 │ True │ 1 │ -1 │ False │
|
829
|
+
│ 0 │ q0 == 'yes' │ 2 │ 0 │ False │
|
830
|
+
│ 1 │ True │ 2 │ -1 │ False │
|
831
|
+
│ 2 │ True │ 3 │ -1 │ False │
|
832
|
+
└───────────┴─────────────┴────────┴──────────┴─────────────┘
|
833
|
+
"""
|
834
|
+
self.rule_collection.show_rules()
|
835
|
+
|
439
836
|
def add_stop_rule(
|
440
837
|
self, question: Union[QuestionBase, str], expression: str
|
441
838
|
) -> Survey:
|
@@ -444,6 +841,7 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
444
841
|
:param question: The question to add the stop rule to.
|
445
842
|
:param expression: The expression to evaluate.
|
446
843
|
|
844
|
+
If this rule is true, the survey ends.
|
447
845
|
The rule is evaluated *after* the question is answered. If the rule is true, the survey ends.
|
448
846
|
|
449
847
|
Here, answering "yes" to q0 ends the survey:
|
@@ -456,10 +854,41 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
456
854
|
|
457
855
|
>>> s.next_question("q0", {"q0": "no"}).question_name
|
458
856
|
'q1'
|
857
|
+
|
858
|
+
>>> s.add_stop_rule("q0", "q1 <> 'yes'")
|
859
|
+
Traceback (most recent call last):
|
860
|
+
...
|
861
|
+
ValueError: The expression contains '<>', which is not allowed. You probably mean '!='.
|
459
862
|
"""
|
863
|
+
expression = ValidatedString(expression)
|
460
864
|
self.add_rule(question, expression, EndOfSurvey)
|
461
865
|
return self
|
462
866
|
|
867
|
+
def clear_non_default_rules(self) -> Survey:
|
868
|
+
"""Remove all non-default rules from the survey.
|
869
|
+
>>> Survey.example().show_rules()
|
870
|
+
┏━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━┓
|
871
|
+
┃ current_q ┃ expression ┃ next_q ┃ priority ┃ before_rule ┃
|
872
|
+
┡━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━┩
|
873
|
+
│ 0 │ True │ 1 │ -1 │ False │
|
874
|
+
│ 0 │ q0 == 'yes' │ 2 │ 0 │ False │
|
875
|
+
│ 1 │ True │ 2 │ -1 │ False │
|
876
|
+
│ 2 │ True │ 3 │ -1 │ False │
|
877
|
+
└───────────┴─────────────┴────────┴──────────┴─────────────┘
|
878
|
+
>>> Survey.example().clear_non_default_rules().show_rules()
|
879
|
+
┏━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━┓
|
880
|
+
┃ current_q ┃ expression ┃ next_q ┃ priority ┃ before_rule ┃
|
881
|
+
┡━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━┩
|
882
|
+
│ 0 │ True │ 1 │ -1 │ False │
|
883
|
+
│ 1 │ True │ 2 │ -1 │ False │
|
884
|
+
│ 2 │ True │ 3 │ -1 │ False │
|
885
|
+
└───────────┴────────────┴────────┴──────────┴─────────────┘
|
886
|
+
"""
|
887
|
+
s = Survey()
|
888
|
+
for question in self.questions:
|
889
|
+
s.add_question(question)
|
890
|
+
return s
|
891
|
+
|
463
892
|
def add_skip_rule(
|
464
893
|
self, question: Union[QuestionBase, str], expression: str
|
465
894
|
) -> Survey:
|
@@ -487,36 +916,6 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
487
916
|
self._add_rule(question, expression, question_index + 1, before_rule=True)
|
488
917
|
return self
|
489
918
|
|
490
|
-
def _get_question_index(
|
491
|
-
self, q: Union[QuestionBase, str, EndOfSurvey.__class__]
|
492
|
-
) -> Union[int, EndOfSurvey.__class__]:
|
493
|
-
"""Return the index of the question or EndOfSurvey object.
|
494
|
-
|
495
|
-
:param q: The question or question name to get the index of.
|
496
|
-
|
497
|
-
It can handle it if the user passes in the question name, the question object, or the EndOfSurvey object.
|
498
|
-
|
499
|
-
>>> s = Survey.example()
|
500
|
-
>>> s._get_question_index("q0")
|
501
|
-
0
|
502
|
-
|
503
|
-
This doesnt' work with questions that don't exist:
|
504
|
-
|
505
|
-
>>> s._get_question_index("poop")
|
506
|
-
Traceback (most recent call last):
|
507
|
-
...
|
508
|
-
ValueError: Question name poop not found in survey. The current question names are {'q0': 0, 'q1': 1, 'q2': 2}.
|
509
|
-
"""
|
510
|
-
if q == EndOfSurvey:
|
511
|
-
return EndOfSurvey
|
512
|
-
else:
|
513
|
-
question_name = q if isinstance(q, str) else q.question_name
|
514
|
-
if question_name not in self.question_name_to_index:
|
515
|
-
raise ValueError(
|
516
|
-
f"""Question name {question_name} not found in survey. The current question names are {self.question_name_to_index}."""
|
517
|
-
)
|
518
|
-
return self.question_name_to_index[question_name]
|
519
|
-
|
520
919
|
def _get_new_rule_priority(
|
521
920
|
self, question_index: int, before_rule: bool = False
|
522
921
|
) -> int:
|
@@ -615,9 +1014,9 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
615
1014
|
|
616
1015
|
return self
|
617
1016
|
|
618
|
-
|
619
|
-
|
620
|
-
|
1017
|
+
# endregion
|
1018
|
+
|
1019
|
+
# region: Forward methods
|
621
1020
|
def by(self, *args: Union["Agent", "Scenario", "LanguageModel"]) -> "Jobs":
|
622
1021
|
"""Add Agents, Scenarios, and LanguageModels to a survey and returns a runnable Jobs object.
|
623
1022
|
|
@@ -640,34 +1039,63 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
640
1039
|
|
641
1040
|
return Jobs(survey=self)
|
642
1041
|
|
1042
|
+
# endregion
|
1043
|
+
|
1044
|
+
# region: Running the survey
|
1045
|
+
|
1046
|
+
def __call__(self, model=None, agent=None, cache=None, **kwargs):
|
1047
|
+
"""Run the survey with default model, taking the required survey as arguments.
|
1048
|
+
|
1049
|
+
>>> from edsl.questions import QuestionFunctional
|
1050
|
+
>>> def f(scenario, agent_traits): return "yes" if scenario["period"] == "morning" else "no"
|
1051
|
+
>>> q = QuestionFunctional(question_name = "q0", func = f)
|
1052
|
+
>>> s = Survey([q])
|
1053
|
+
>>> s(period = "morning", cache = False).select("answer.q0").first()
|
1054
|
+
'yes'
|
1055
|
+
>>> s(period = "evening", cache = False).select("answer.q0").first()
|
1056
|
+
'no'
|
1057
|
+
"""
|
1058
|
+
job = self.get_job(model, agent, **kwargs)
|
1059
|
+
return job.run(cache=cache)
|
1060
|
+
|
1061
|
+
async def run_async(self, model=None, agent=None, cache=None, **kwargs):
|
1062
|
+
"""Run the survey with default model, taking the required survey as arguments.
|
1063
|
+
|
1064
|
+
>>> from edsl.questions import QuestionFunctional
|
1065
|
+
>>> def f(scenario, agent_traits): return "yes" if scenario["period"] == "morning" else "no"
|
1066
|
+
>>> q = QuestionFunctional(question_name = "q0", func = f)
|
1067
|
+
>>> s = Survey([q])
|
1068
|
+
>>> s(period = "morning").select("answer.q0").first()
|
1069
|
+
'yes'
|
1070
|
+
>>> s(period = "evening").select("answer.q0").first()
|
1071
|
+
'no'
|
1072
|
+
"""
|
1073
|
+
# TODO: temp fix by creating a cache
|
1074
|
+
if cache is None:
|
1075
|
+
from edsl.data import Cache
|
1076
|
+
|
1077
|
+
c = Cache()
|
1078
|
+
else:
|
1079
|
+
c = cache
|
1080
|
+
jobs: "Jobs" = self.get_job(model, agent, **kwargs)
|
1081
|
+
return await jobs.run_async(cache=c)
|
1082
|
+
|
643
1083
|
def run(self, *args, **kwargs) -> "Results":
|
644
1084
|
"""Turn the survey into a Job and runs it.
|
645
1085
|
|
646
|
-
Here we run a survey but with debug mode on (so LLM calls are not made)
|
647
|
-
|
648
1086
|
>>> from edsl import QuestionFreeText
|
649
1087
|
>>> s = Survey([QuestionFreeText.example()])
|
650
|
-
>>>
|
651
|
-
>>>
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
┡━━━━━━━━━━━━━━┩
|
656
|
-
...
|
657
|
-
└──────────────┘
|
1088
|
+
>>> from edsl.language_models import LanguageModel
|
1089
|
+
>>> m = LanguageModel.example(test_model = True, canned_response = "Great!")
|
1090
|
+
>>> results = s.by(m).run(cache = False)
|
1091
|
+
>>> results.select('answer.*')
|
1092
|
+
Dataset([{'answer.how_are_you': ['Great!']}])
|
658
1093
|
"""
|
659
1094
|
from edsl.jobs.Jobs import Jobs
|
660
1095
|
|
661
1096
|
return Jobs(survey=self).run(*args, **kwargs)
|
662
1097
|
|
663
|
-
|
664
|
-
## Survey-Taking Methods
|
665
|
-
########################
|
666
|
-
|
667
|
-
def _first_question(self) -> QuestionBase:
|
668
|
-
"""Return the first question in the survey."""
|
669
|
-
return self.questions[0]
|
670
|
-
|
1098
|
+
# region: Survey flow
|
671
1099
|
def next_question(
|
672
1100
|
self, current_question: Union[str, QuestionBase], answers: dict
|
673
1101
|
) -> Union[QuestionBase, EndOfSurvey.__class__]:
|
@@ -747,7 +1175,7 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
747
1175
|
Question('multiple_choice', question_name = \"""q1\""", question_text = \"""Why not?\""", question_options = ['killer bees in cafeteria', 'other'])
|
748
1176
|
"""
|
749
1177
|
self.answers = {}
|
750
|
-
question = self.
|
1178
|
+
question = self._questions[0]
|
751
1179
|
while not question == EndOfSurvey:
|
752
1180
|
# breakpoint()
|
753
1181
|
answer = yield question
|
@@ -756,34 +1184,9 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
756
1184
|
## TODO: This should also include survey and agent attributes
|
757
1185
|
question = self.next_question(question, self.answers)
|
758
1186
|
|
759
|
-
|
760
|
-
def scenario_attributes(self) -> list[str]:
|
761
|
-
"""Return a list of attributes that admissible Scenarios should have.
|
762
|
-
|
763
|
-
Here we have a survey with a question that uses a jinja2 style {{ }} template:
|
764
|
-
|
765
|
-
>>> from edsl import QuestionFreeText
|
766
|
-
>>> s = Survey().add_question(QuestionFreeText(question_text="{{ greeting }}. What is your name?", question_name="name"))
|
767
|
-
>>> s.scenario_attributes
|
768
|
-
['greeting']
|
769
|
-
|
770
|
-
>>> s = Survey().add_question(QuestionFreeText(question_text="{{ greeting }}. What is your {{ attribute }}?", question_name="name"))
|
771
|
-
>>> s.scenario_attributes
|
772
|
-
['greeting', 'attribute']
|
773
|
-
|
774
|
-
|
775
|
-
"""
|
776
|
-
temp = []
|
777
|
-
for question in self.questions:
|
778
|
-
question_text = question.question_text
|
779
|
-
# extract the contents of all {{ }} in the question text using regex
|
780
|
-
matches = re.findall(r"\{\{(.+?)\}\}", question_text)
|
781
|
-
# remove whitespace
|
782
|
-
matches = [match.strip() for match in matches]
|
783
|
-
# add them to the temp list
|
784
|
-
temp.extend(matches)
|
785
|
-
return temp
|
1187
|
+
# endregion
|
786
1188
|
|
1189
|
+
# regions: DAG construction
|
787
1190
|
def textify(self, index_dag: DAG) -> DAG:
|
788
1191
|
"""Convert the DAG of question indices to a DAG of question names.
|
789
1192
|
|
@@ -923,59 +1326,6 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
923
1326
|
return False
|
924
1327
|
return self.to_dict() == other.to_dict()
|
925
1328
|
|
926
|
-
###################
|
927
|
-
# SERIALIZATION METHODS
|
928
|
-
###################
|
929
|
-
|
930
|
-
def _to_dict(self) -> dict[str, Any]:
|
931
|
-
"""Serialize the Survey object to a dictionary.
|
932
|
-
|
933
|
-
>>> s = Survey.example()
|
934
|
-
>>> s._to_dict().keys()
|
935
|
-
dict_keys(['questions', 'memory_plan', 'rule_collection', 'question_groups'])
|
936
|
-
|
937
|
-
"""
|
938
|
-
return {
|
939
|
-
"questions": [q._to_dict() for q in self._questions],
|
940
|
-
"memory_plan": self.memory_plan.to_dict(),
|
941
|
-
"rule_collection": self.rule_collection.to_dict(),
|
942
|
-
"question_groups": self.question_groups,
|
943
|
-
}
|
944
|
-
|
945
|
-
@add_edsl_version
|
946
|
-
def to_dict(self) -> dict[str, Any]:
|
947
|
-
"""Serialize the Survey object to a dictionary.
|
948
|
-
|
949
|
-
>>> s = Survey.example()
|
950
|
-
>>> s.to_dict().keys()
|
951
|
-
dict_keys(['questions', 'memory_plan', 'rule_collection', 'question_groups', 'edsl_version', 'edsl_class_name'])
|
952
|
-
|
953
|
-
"""
|
954
|
-
return self._to_dict()
|
955
|
-
|
956
|
-
@classmethod
|
957
|
-
@remove_edsl_version
|
958
|
-
def from_dict(cls, data: dict) -> Survey:
|
959
|
-
"""Deserialize the dictionary back to a Survey object.
|
960
|
-
|
961
|
-
:param data: The dictionary to deserialize.
|
962
|
-
|
963
|
-
>>> d = Survey.example().to_dict()
|
964
|
-
>>> s = Survey.from_dict(d)
|
965
|
-
>>> s == Survey.example()
|
966
|
-
True
|
967
|
-
|
968
|
-
"""
|
969
|
-
questions = [QuestionBase.from_dict(q_dict) for q_dict in data["questions"]]
|
970
|
-
memory_plan = MemoryPlan.from_dict(data["memory_plan"])
|
971
|
-
survey = cls(
|
972
|
-
questions=questions,
|
973
|
-
memory_plan=memory_plan,
|
974
|
-
rule_collection=RuleCollection.from_dict(data["rule_collection"]),
|
975
|
-
question_groups=data["question_groups"],
|
976
|
-
)
|
977
|
-
return survey
|
978
|
-
|
979
1329
|
@classmethod
|
980
1330
|
def from_qsf(
|
981
1331
|
cls, qsf_file: Optional[str] = None, url: Optional[str] = None
|
@@ -1002,9 +1352,7 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1002
1352
|
so = SurveyQualtricsImport(qsf_file)
|
1003
1353
|
return so.create_survey()
|
1004
1354
|
|
1005
|
-
|
1006
|
-
# DISPLAY METHODS
|
1007
|
-
###################
|
1355
|
+
# region: Display methods
|
1008
1356
|
def print(self):
|
1009
1357
|
"""Print the survey in a rich format.
|
1010
1358
|
|
@@ -1023,7 +1371,8 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1023
1371
|
def __repr__(self) -> str:
|
1024
1372
|
"""Return a string representation of the survey."""
|
1025
1373
|
|
1026
|
-
questions_string = ", ".join([repr(q) for q in self._questions])
|
1374
|
+
# questions_string = ", ".join([repr(q) for q in self._questions])
|
1375
|
+
questions_string = ", ".join([repr(q) for q in self.raw_passed_questions or []])
|
1027
1376
|
# question_names_string = ", ".join([repr(name) for name in self.question_names])
|
1028
1377
|
return f"Survey(questions=[{questions_string}], memory_plan={self.memory_plan}, rule_collection={self.rule_collection}, question_groups={self.question_groups})"
|
1029
1378
|
|
@@ -1032,22 +1381,6 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1032
1381
|
|
1033
1382
|
return data_to_html(self.to_dict())
|
1034
1383
|
|
1035
|
-
def show_rules(self) -> None:
|
1036
|
-
"""Print out the rules in the survey.
|
1037
|
-
|
1038
|
-
>>> s = Survey.example()
|
1039
|
-
>>> s.show_rules()
|
1040
|
-
┏━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━┓
|
1041
|
-
┃ current_q ┃ expression ┃ next_q ┃ priority ┃ before_rule ┃
|
1042
|
-
┡━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━┩
|
1043
|
-
│ 0 │ True │ 1 │ -1 │ False │
|
1044
|
-
│ 0 │ q0 == 'yes' │ 2 │ 0 │ False │
|
1045
|
-
│ 1 │ True │ 2 │ -1 │ False │
|
1046
|
-
│ 2 │ True │ 3 │ -1 │ False │
|
1047
|
-
└───────────┴─────────────┴────────┴──────────┴─────────────┘
|
1048
|
-
"""
|
1049
|
-
self.rule_collection.show_rules()
|
1050
|
-
|
1051
1384
|
def rich_print(self) -> Table:
|
1052
1385
|
"""Print the survey in a rich format.
|
1053
1386
|
|
@@ -1083,6 +1416,8 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1083
1416
|
|
1084
1417
|
return table
|
1085
1418
|
|
1419
|
+
# endregion
|
1420
|
+
|
1086
1421
|
def codebook(self) -> dict[str, str]:
|
1087
1422
|
"""Create a codebook for the survey, mapping question names to question text.
|
1088
1423
|
|
@@ -1095,6 +1430,7 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1095
1430
|
codebook[question.question_name] = question.question_text
|
1096
1431
|
return codebook
|
1097
1432
|
|
1433
|
+
# region: Export methods
|
1098
1434
|
def to_csv(self, filename: str = None):
|
1099
1435
|
"""Export the survey to a CSV file.
|
1100
1436
|
|
@@ -1137,8 +1473,16 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1137
1473
|
res = c.web(self.to_dict(), platform, email)
|
1138
1474
|
return res
|
1139
1475
|
|
1476
|
+
# endregion
|
1477
|
+
|
1140
1478
|
@classmethod
|
1141
|
-
def example(
|
1479
|
+
def example(
|
1480
|
+
cls,
|
1481
|
+
params: bool = False,
|
1482
|
+
randomize: bool = False,
|
1483
|
+
include_instructions=False,
|
1484
|
+
custom_instructions: Optional[str] = None,
|
1485
|
+
) -> Survey:
|
1142
1486
|
"""Return an example survey.
|
1143
1487
|
|
1144
1488
|
>>> s = Survey.example()
|
@@ -1171,6 +1515,18 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1171
1515
|
)
|
1172
1516
|
s = cls(questions=[q0, q1, q2, q3])
|
1173
1517
|
return s
|
1518
|
+
|
1519
|
+
if include_instructions:
|
1520
|
+
from edsl import Instruction
|
1521
|
+
|
1522
|
+
custom_instructions = (
|
1523
|
+
custom_instructions if custom_instructions else "Please pay attention!"
|
1524
|
+
)
|
1525
|
+
|
1526
|
+
i = Instruction(text=custom_instructions, name="attention")
|
1527
|
+
s = cls(questions=[i, q0, q1, q2])
|
1528
|
+
return s
|
1529
|
+
|
1174
1530
|
s = cls(questions=[q0, q1, q2])
|
1175
1531
|
s = s.add_rule(q0, "q0 == 'yes'", q2)
|
1176
1532
|
return s
|
@@ -1192,43 +1548,6 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1192
1548
|
|
1193
1549
|
return self.by(s).by(agent).by(model)
|
1194
1550
|
|
1195
|
-
def __call__(self, model=None, agent=None, cache=None, **kwargs):
|
1196
|
-
"""Run the survey with default model, taking the required survey as arguments.
|
1197
|
-
|
1198
|
-
>>> from edsl.questions import QuestionFunctional
|
1199
|
-
>>> def f(scenario, agent_traits): return "yes" if scenario["period"] == "morning" else "no"
|
1200
|
-
>>> q = QuestionFunctional(question_name = "q0", func = f)
|
1201
|
-
>>> s = Survey([q])
|
1202
|
-
>>> s(period = "morning", cache = False).select("answer.q0").first()
|
1203
|
-
'yes'
|
1204
|
-
>>> s(period = "evening", cache = False).select("answer.q0").first()
|
1205
|
-
'no'
|
1206
|
-
"""
|
1207
|
-
job = self.get_job(model, agent, **kwargs)
|
1208
|
-
return job.run(cache=cache)
|
1209
|
-
|
1210
|
-
async def run_async(self, model=None, agent=None, cache=None, **kwargs):
|
1211
|
-
"""Run the survey with default model, taking the required survey as arguments.
|
1212
|
-
|
1213
|
-
>>> from edsl.questions import QuestionFunctional
|
1214
|
-
>>> def f(scenario, agent_traits): return "yes" if scenario["period"] == "morning" else "no"
|
1215
|
-
>>> q = QuestionFunctional(question_name = "q0", func = f)
|
1216
|
-
>>> s = Survey([q])
|
1217
|
-
>>> s(period = "morning").select("answer.q0").first()
|
1218
|
-
'yes'
|
1219
|
-
>>> s(period = "evening").select("answer.q0").first()
|
1220
|
-
'no'
|
1221
|
-
"""
|
1222
|
-
# TODO: temp fix by creating a cache
|
1223
|
-
if cache is None:
|
1224
|
-
from edsl.data import Cache
|
1225
|
-
|
1226
|
-
c = Cache()
|
1227
|
-
else:
|
1228
|
-
c = cache
|
1229
|
-
jobs: "Jobs" = self.get_job(model, agent, **kwargs)
|
1230
|
-
return await jobs.run_async(cache=c)
|
1231
|
-
|
1232
1551
|
|
1233
1552
|
def main():
|
1234
1553
|
"""Run the example survey."""
|