edsl 0.1.33__py3-none-any.whl → 0.1.33.dev1__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 +3 -9
- edsl/__init__.py +3 -8
- edsl/__version__.py +1 -1
- edsl/agents/Agent.py +8 -40
- edsl/agents/AgentList.py +0 -43
- edsl/agents/Invigilator.py +219 -135
- edsl/agents/InvigilatorBase.py +59 -148
- edsl/agents/{PromptConstructor.py → PromptConstructionMixin.py} +89 -138
- edsl/agents/__init__.py +0 -1
- edsl/config.py +56 -47
- edsl/coop/coop.py +7 -50
- edsl/data/Cache.py +1 -35
- edsl/data_transfer_models.py +38 -73
- edsl/enums.py +0 -4
- edsl/exceptions/language_models.py +1 -25
- edsl/exceptions/questions.py +5 -62
- edsl/exceptions/results.py +0 -4
- edsl/inference_services/AnthropicService.py +11 -13
- edsl/inference_services/AwsBedrock.py +17 -19
- edsl/inference_services/AzureAI.py +20 -37
- edsl/inference_services/GoogleService.py +12 -16
- edsl/inference_services/GroqService.py +0 -2
- edsl/inference_services/InferenceServiceABC.py +3 -58
- edsl/inference_services/OpenAIService.py +54 -48
- edsl/inference_services/models_available_cache.py +6 -0
- edsl/inference_services/registry.py +0 -6
- edsl/jobs/Answers.py +12 -10
- edsl/jobs/Jobs.py +21 -36
- edsl/jobs/buckets/BucketCollection.py +15 -24
- edsl/jobs/buckets/TokenBucket.py +14 -93
- edsl/jobs/interviews/Interview.py +78 -366
- edsl/jobs/interviews/InterviewExceptionEntry.py +19 -85
- edsl/jobs/interviews/InterviewTaskBuildingMixin.py +286 -0
- edsl/jobs/interviews/{InterviewExceptionCollection.py → interview_exception_tracking.py} +68 -14
- edsl/jobs/interviews/retry_management.py +37 -0
- edsl/jobs/runners/JobsRunnerAsyncio.py +175 -146
- edsl/jobs/runners/JobsRunnerStatusMixin.py +333 -0
- edsl/jobs/tasks/QuestionTaskCreator.py +23 -30
- edsl/jobs/tasks/TaskHistory.py +213 -148
- edsl/language_models/LanguageModel.py +156 -261
- edsl/language_models/ModelList.py +2 -2
- edsl/language_models/RegisterLanguageModelsMeta.py +29 -14
- edsl/language_models/registry.py +6 -23
- edsl/language_models/repair.py +19 -0
- edsl/prompts/Prompt.py +2 -52
- edsl/questions/AnswerValidatorMixin.py +26 -23
- edsl/questions/QuestionBase.py +249 -329
- edsl/questions/QuestionBudget.py +41 -99
- edsl/questions/QuestionCheckBox.py +35 -227
- edsl/questions/QuestionExtract.py +27 -98
- edsl/questions/QuestionFreeText.py +29 -52
- edsl/questions/QuestionFunctional.py +0 -7
- edsl/questions/QuestionList.py +22 -141
- edsl/questions/QuestionMultipleChoice.py +65 -159
- edsl/questions/QuestionNumerical.py +46 -88
- edsl/questions/QuestionRank.py +24 -182
- edsl/questions/RegisterQuestionsMeta.py +12 -31
- edsl/questions/__init__.py +4 -3
- edsl/questions/derived/QuestionLikertFive.py +5 -10
- edsl/questions/derived/QuestionLinearScale.py +2 -15
- edsl/questions/derived/QuestionTopK.py +1 -10
- edsl/questions/derived/QuestionYesNo.py +3 -24
- edsl/questions/descriptors.py +7 -43
- edsl/questions/question_registry.py +2 -6
- edsl/results/Dataset.py +0 -20
- edsl/results/DatasetExportMixin.py +48 -46
- edsl/results/Result.py +5 -32
- edsl/results/Results.py +46 -135
- edsl/results/ResultsDBMixin.py +3 -3
- edsl/scenarios/FileStore.py +10 -71
- edsl/scenarios/Scenario.py +25 -96
- edsl/scenarios/ScenarioImageMixin.py +2 -2
- edsl/scenarios/ScenarioList.py +39 -361
- edsl/scenarios/ScenarioListExportMixin.py +0 -9
- edsl/scenarios/ScenarioListPdfMixin.py +4 -150
- edsl/study/SnapShot.py +1 -8
- edsl/study/Study.py +0 -32
- edsl/surveys/Rule.py +1 -10
- edsl/surveys/RuleCollection.py +5 -21
- edsl/surveys/Survey.py +310 -636
- edsl/surveys/SurveyExportMixin.py +9 -71
- edsl/surveys/SurveyFlowVisualizationMixin.py +1 -2
- edsl/surveys/SurveyQualtricsImport.py +4 -75
- edsl/utilities/gcp_bucket/simple_example.py +9 -0
- edsl/utilities/utilities.py +1 -9
- {edsl-0.1.33.dist-info → edsl-0.1.33.dev1.dist-info}/METADATA +2 -5
- edsl-0.1.33.dev1.dist-info/RECORD +209 -0
- edsl/TemplateLoader.py +0 -24
- edsl/auto/AutoStudy.py +0 -117
- edsl/auto/StageBase.py +0 -230
- edsl/auto/StageGenerateSurvey.py +0 -178
- edsl/auto/StageLabelQuestions.py +0 -125
- edsl/auto/StagePersona.py +0 -61
- edsl/auto/StagePersonaDimensionValueRanges.py +0 -88
- edsl/auto/StagePersonaDimensionValues.py +0 -74
- edsl/auto/StagePersonaDimensions.py +0 -69
- edsl/auto/StageQuestions.py +0 -73
- edsl/auto/SurveyCreatorPipeline.py +0 -21
- edsl/auto/utilities.py +0 -224
- edsl/coop/PriceFetcher.py +0 -58
- edsl/inference_services/MistralAIService.py +0 -120
- edsl/inference_services/TestService.py +0 -80
- edsl/inference_services/TogetherAIService.py +0 -170
- edsl/jobs/FailedQuestion.py +0 -78
- edsl/jobs/runners/JobsRunnerStatus.py +0 -331
- edsl/language_models/fake_openai_call.py +0 -15
- edsl/language_models/fake_openai_service.py +0 -61
- edsl/language_models/utilities.py +0 -61
- edsl/questions/QuestionBaseGenMixin.py +0 -133
- edsl/questions/QuestionBasePromptsMixin.py +0 -266
- edsl/questions/Quick.py +0 -41
- edsl/questions/ResponseValidatorABC.py +0 -170
- edsl/questions/decorators.py +0 -21
- edsl/questions/prompt_templates/question_budget.jinja +0 -13
- edsl/questions/prompt_templates/question_checkbox.jinja +0 -32
- edsl/questions/prompt_templates/question_extract.jinja +0 -11
- edsl/questions/prompt_templates/question_free_text.jinja +0 -3
- edsl/questions/prompt_templates/question_linear_scale.jinja +0 -11
- edsl/questions/prompt_templates/question_list.jinja +0 -17
- edsl/questions/prompt_templates/question_multiple_choice.jinja +0 -33
- edsl/questions/prompt_templates/question_numerical.jinja +0 -37
- edsl/questions/templates/__init__.py +0 -0
- edsl/questions/templates/budget/__init__.py +0 -0
- edsl/questions/templates/budget/answering_instructions.jinja +0 -7
- edsl/questions/templates/budget/question_presentation.jinja +0 -7
- edsl/questions/templates/checkbox/__init__.py +0 -0
- edsl/questions/templates/checkbox/answering_instructions.jinja +0 -10
- edsl/questions/templates/checkbox/question_presentation.jinja +0 -22
- edsl/questions/templates/extract/__init__.py +0 -0
- edsl/questions/templates/extract/answering_instructions.jinja +0 -7
- edsl/questions/templates/extract/question_presentation.jinja +0 -1
- 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 +0 -1
- edsl/questions/templates/likert_five/__init__.py +0 -0
- edsl/questions/templates/likert_five/answering_instructions.jinja +0 -10
- edsl/questions/templates/likert_five/question_presentation.jinja +0 -12
- edsl/questions/templates/linear_scale/__init__.py +0 -0
- edsl/questions/templates/linear_scale/answering_instructions.jinja +0 -5
- edsl/questions/templates/linear_scale/question_presentation.jinja +0 -5
- edsl/questions/templates/list/__init__.py +0 -0
- edsl/questions/templates/list/answering_instructions.jinja +0 -4
- edsl/questions/templates/list/question_presentation.jinja +0 -5
- edsl/questions/templates/multiple_choice/__init__.py +0 -0
- edsl/questions/templates/multiple_choice/answering_instructions.jinja +0 -9
- edsl/questions/templates/multiple_choice/html.jinja +0 -0
- edsl/questions/templates/multiple_choice/question_presentation.jinja +0 -12
- edsl/questions/templates/numerical/__init__.py +0 -0
- edsl/questions/templates/numerical/answering_instructions.jinja +0 -8
- edsl/questions/templates/numerical/question_presentation.jinja +0 -7
- edsl/questions/templates/rank/__init__.py +0 -0
- edsl/questions/templates/rank/answering_instructions.jinja +0 -11
- edsl/questions/templates/rank/question_presentation.jinja +0 -15
- edsl/questions/templates/top_k/__init__.py +0 -0
- edsl/questions/templates/top_k/answering_instructions.jinja +0 -8
- edsl/questions/templates/top_k/question_presentation.jinja +0 -22
- edsl/questions/templates/yes_no/__init__.py +0 -0
- edsl/questions/templates/yes_no/answering_instructions.jinja +0 -6
- edsl/questions/templates/yes_no/question_presentation.jinja +0 -12
- edsl/results/DatasetTree.py +0 -145
- edsl/results/Selector.py +0 -118
- edsl/results/tree_explore.py +0 -115
- edsl/surveys/instructions/ChangeInstruction.py +0 -47
- edsl/surveys/instructions/Instruction.py +0 -34
- edsl/surveys/instructions/InstructionCollection.py +0 -77
- edsl/surveys/instructions/__init__.py +0 -0
- edsl/templates/error_reporting/base.html +0 -24
- edsl/templates/error_reporting/exceptions_by_model.html +0 -35
- edsl/templates/error_reporting/exceptions_by_question_name.html +0 -17
- edsl/templates/error_reporting/exceptions_by_type.html +0 -17
- edsl/templates/error_reporting/interview_details.html +0 -116
- edsl/templates/error_reporting/interviews.html +0 -10
- edsl/templates/error_reporting/overview.html +0 -5
- edsl/templates/error_reporting/performance_plot.html +0 -2
- edsl/templates/error_reporting/report.css +0 -74
- edsl/templates/error_reporting/report.html +0 -118
- edsl/templates/error_reporting/report.js +0 -25
- edsl-0.1.33.dist-info/RECORD +0 -295
- {edsl-0.1.33.dist-info → edsl-0.1.33.dev1.dist-info}/LICENSE +0 -0
- {edsl-0.1.33.dist-info → edsl-0.1.33.dev1.dist-info}/WHEEL +0 -0
edsl/surveys/Survey.py
CHANGED
@@ -20,24 +20,6 @@ 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
|
-
|
41
23
|
|
42
24
|
class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
43
25
|
"""A collection of questions that supports skip logic."""
|
@@ -60,13 +42,11 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
60
42
|
|
61
43
|
def __init__(
|
62
44
|
self,
|
63
|
-
questions: Optional[
|
64
|
-
list[Union[QuestionBase, Instruction, ChangeInstruction]]
|
65
|
-
] = None,
|
45
|
+
questions: Optional[list[QuestionBase]] = None,
|
66
46
|
memory_plan: Optional[MemoryPlan] = None,
|
67
47
|
rule_collection: Optional[RuleCollection] = None,
|
68
48
|
question_groups: Optional[dict[str, tuple[int, int]]] = None,
|
69
|
-
name:
|
49
|
+
name: str = None,
|
70
50
|
):
|
71
51
|
"""Create a new survey.
|
72
52
|
|
@@ -76,33 +56,13 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
76
56
|
:param question_groups: The groups of questions in the survey.
|
77
57
|
:param name: The name of the survey - DEPRECATED.
|
78
58
|
|
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
|
-
|
87
59
|
"""
|
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
|
-
|
97
60
|
self.rule_collection = RuleCollection(
|
98
|
-
num_questions=len(
|
61
|
+
num_questions=len(questions) if questions else None
|
99
62
|
)
|
100
63
|
# the RuleCollection needs to be present while we add the questions; we might override this later
|
101
64
|
# if a rule_collection is provided. This allows us to serialize the survey with the rule_collection.
|
102
|
-
|
103
|
-
self.questions = true_questions
|
104
|
-
self.instruction_names_to_instructions = instruction_names_to_instructions
|
105
|
-
|
65
|
+
self.questions = questions or []
|
106
66
|
self.memory_plan = memory_plan or MemoryPlan(self)
|
107
67
|
if question_groups is not None:
|
108
68
|
self.question_groups = question_groups
|
@@ -118,177 +78,6 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
118
78
|
|
119
79
|
warnings.warn("name parameter to a survey is deprecated.")
|
120
80
|
|
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
|
-
|
292
81
|
def simulate(self) -> dict:
|
293
82
|
"""Simulate the survey and return the answers."""
|
294
83
|
i = self.gen_path_through_survey()
|
@@ -304,6 +93,9 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
304
93
|
def create_agent(self) -> "Agent":
|
305
94
|
"""Create an agent from the simulated answers."""
|
306
95
|
answers_dict = self.simulate()
|
96
|
+
from edsl.agents.Agent import Agent
|
97
|
+
|
98
|
+
a = Agent(traits=answers_dict)
|
307
99
|
|
308
100
|
def construct_answer_dict_function(traits: dict) -> Callable:
|
309
101
|
def func(self, question: "QuestionBase", scenario=None):
|
@@ -311,48 +103,16 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
311
103
|
|
312
104
|
return func
|
313
105
|
|
314
|
-
|
106
|
+
a.add_direct_question_answering_method(
|
315
107
|
construct_answer_dict_function(answers_dict)
|
316
108
|
)
|
109
|
+
return a
|
317
110
|
|
318
111
|
def simulate_results(self) -> "Results":
|
319
112
|
"""Simulate the survey and return the results."""
|
320
113
|
a = self.create_agent()
|
321
114
|
return self.by([a]).run()
|
322
115
|
|
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
|
-
|
356
116
|
def get(self, question_name: str) -> QuestionBase:
|
357
117
|
"""
|
358
118
|
Return the question object given the question name.
|
@@ -368,184 +128,22 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
368
128
|
index = self.question_name_to_index[question_name]
|
369
129
|
return self._questions[index]
|
370
130
|
|
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
|
+
|
371
135
|
def get_question(self, question_name: str) -> QuestionBase:
|
372
136
|
"""Return the question object given the question name."""
|
373
137
|
# import warnings
|
374
138
|
# warnings.warn("survey.get_question is deprecated. Use subscript operator instead.")
|
375
139
|
return self.get(question_name)
|
376
140
|
|
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
|
409
141
|
def __hash__(self) -> int:
|
410
142
|
"""Return a hash of the question."""
|
411
143
|
from edsl.utilities.utilities import dict_hash
|
412
144
|
|
413
145
|
return dict_hash(self._to_dict())
|
414
146
|
|
415
|
-
def _to_dict(self) -> dict[str, Any]:
|
416
|
-
"""Serialize the Survey object to a dictionary.
|
417
|
-
|
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'])
|
438
|
-
|
439
|
-
"""
|
440
|
-
return self._to_dict()
|
441
|
-
|
442
|
-
@classmethod
|
443
|
-
@remove_edsl_version
|
444
|
-
def from_dict(cls, data: dict) -> Survey:
|
445
|
-
"""Deserialize the dictionary back to a Survey object.
|
446
|
-
|
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 = []
|
513
|
-
for question in self.questions:
|
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
|
522
|
-
|
523
|
-
@property
|
524
|
-
def parameters(self):
|
525
|
-
"""Return a set of parameters in the survey.
|
526
|
-
|
527
|
-
>>> s = Survey.example()
|
528
|
-
>>> s.parameters
|
529
|
-
set()
|
530
|
-
"""
|
531
|
-
return set.union(*[q.parameters for q in self.questions])
|
532
|
-
|
533
|
-
@property
|
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
|
-
"""
|
542
|
-
return {q.question_name: q.parameters for q in self.questions}
|
543
|
-
|
544
|
-
# endregion
|
545
|
-
|
546
|
-
# region: Survey construction
|
547
|
-
|
548
|
-
# region: Adding questions and combining surveys
|
549
147
|
def __add__(self, other: Survey) -> Survey:
|
550
148
|
"""Combine two surveys.
|
551
149
|
|
@@ -573,6 +171,51 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
573
171
|
|
574
172
|
return Survey(questions=self.questions + other.questions)
|
575
173
|
|
174
|
+
def clear_non_default_rules(self) -> Survey:
|
175
|
+
s = Survey()
|
176
|
+
for question in self.questions:
|
177
|
+
s.add_question(question)
|
178
|
+
return s
|
179
|
+
|
180
|
+
@property
|
181
|
+
def parameters(self):
|
182
|
+
"""Return a set of parameters in the survey.
|
183
|
+
|
184
|
+
>>> s = Survey.example()
|
185
|
+
>>> s.parameters
|
186
|
+
set()
|
187
|
+
"""
|
188
|
+
return set.union(*[q.parameters for q in self.questions])
|
189
|
+
|
190
|
+
@property
|
191
|
+
def parameters_by_question(self):
|
192
|
+
return {q.question_name: q.parameters for q in self.questions}
|
193
|
+
|
194
|
+
@property
|
195
|
+
def question_names(self) -> list[str]:
|
196
|
+
"""Return a list of question names in the survey.
|
197
|
+
|
198
|
+
Example:
|
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]
|
206
|
+
|
207
|
+
@property
|
208
|
+
def question_name_to_index(self) -> dict[str, int]:
|
209
|
+
"""Return a dictionary mapping question names to question indices.
|
210
|
+
|
211
|
+
Example:
|
212
|
+
|
213
|
+
>>> s = Survey.example()
|
214
|
+
>>> s.question_name_to_index
|
215
|
+
{'q0': 0, 'q1': 1, 'q2': 2}
|
216
|
+
"""
|
217
|
+
return {q.question_name: i for i, q in enumerate(self.questions)}
|
218
|
+
|
576
219
|
def add_question(self, question: QuestionBase) -> Survey:
|
577
220
|
"""
|
578
221
|
Add a question to survey.
|
@@ -590,11 +233,11 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
590
233
|
>>> s = Survey().add_question(q).add_question(q)
|
591
234
|
Traceback (most recent call last):
|
592
235
|
...
|
593
|
-
edsl.exceptions.surveys.SurveyCreationError: Question name
|
236
|
+
edsl.exceptions.surveys.SurveyCreationError: Question name already exists in survey. ...
|
594
237
|
"""
|
595
238
|
if question.question_name in self.question_names:
|
596
239
|
raise SurveyCreationError(
|
597
|
-
f"""Question name
|
240
|
+
f"""Question name already exists in survey. Please use a different name for the offensing question. The problemetic question name is {question.question_name}."""
|
598
241
|
)
|
599
242
|
index = len(self.questions)
|
600
243
|
# TODO: This is a bit ugly because the user
|
@@ -602,8 +245,6 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
602
245
|
# descriptor.
|
603
246
|
self._questions.append(question)
|
604
247
|
|
605
|
-
self.pseudo_indices[question.question_name] = index
|
606
|
-
|
607
248
|
# using index + 1 presumes there is a next question
|
608
249
|
self.rule_collection.add_rule(
|
609
250
|
Rule(
|
@@ -622,20 +263,6 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
622
263
|
|
623
264
|
return self
|
624
265
|
|
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
|
639
266
|
def set_full_memory_mode(self) -> Survey:
|
640
267
|
"""Add instructions to a survey that the agent should remember all of the answers to the questions in the survey.
|
641
268
|
|
@@ -668,75 +295,6 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
668
295
|
prior_questions=prior_questions_func(i),
|
669
296
|
)
|
670
297
|
|
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
|
740
298
|
def add_question_group(
|
741
299
|
self,
|
742
300
|
start_question: Union[QuestionBase, str],
|
@@ -814,24 +372,69 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
814
372
|
self.question_groups[group_name] = (start_index, end_index)
|
815
373
|
return self
|
816
374
|
|
817
|
-
|
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.
|
818
413
|
|
819
|
-
|
820
|
-
def show_rules(self) -> None:
|
821
|
-
"""Print out the rules in the survey.
|
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.
|
822
415
|
|
823
|
-
|
824
|
-
|
825
|
-
|
826
|
-
|
827
|
-
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
│ 2 │ True │ 3 │ -1 │ False │
|
832
|
-
└───────────┴─────────────┴────────┴──────────┴─────────────┘
|
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'])}
|
833
424
|
"""
|
834
|
-
self.
|
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
|
+
)
|
437
|
+
return self
|
835
438
|
|
836
439
|
def add_stop_rule(
|
837
440
|
self, question: Union[QuestionBase, str], expression: str
|
@@ -841,7 +444,6 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
841
444
|
:param question: The question to add the stop rule to.
|
842
445
|
:param expression: The expression to evaluate.
|
843
446
|
|
844
|
-
If this rule is true, the survey ends.
|
845
447
|
The rule is evaluated *after* the question is answered. If the rule is true, the survey ends.
|
846
448
|
|
847
449
|
Here, answering "yes" to q0 ends the survey:
|
@@ -854,42 +456,10 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
854
456
|
|
855
457
|
>>> s.next_question("q0", {"q0": "no"}).question_name
|
856
458
|
'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 '!='.
|
862
459
|
"""
|
863
|
-
expression = ValidatedString(expression)
|
864
460
|
self.add_rule(question, expression, EndOfSurvey)
|
865
461
|
return self
|
866
462
|
|
867
|
-
def clear_non_default_rules(self) -> Survey:
|
868
|
-
"""Remove all non-default rules from the survey.
|
869
|
-
|
870
|
-
>>> Survey.example().show_rules()
|
871
|
-
┏━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━┓
|
872
|
-
┃ current_q ┃ expression ┃ next_q ┃ priority ┃ before_rule ┃
|
873
|
-
┡━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━┩
|
874
|
-
│ 0 │ True │ 1 │ -1 │ False │
|
875
|
-
│ 0 │ q0 == 'yes' │ 2 │ 0 │ False │
|
876
|
-
│ 1 │ True │ 2 │ -1 │ False │
|
877
|
-
│ 2 │ True │ 3 │ -1 │ False │
|
878
|
-
└───────────┴─────────────┴────────┴──────────┴─────────────┘
|
879
|
-
>>> Survey.example().clear_non_default_rules().show_rules()
|
880
|
-
┏━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━┓
|
881
|
-
┃ current_q ┃ expression ┃ next_q ┃ priority ┃ before_rule ┃
|
882
|
-
┡━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━┩
|
883
|
-
│ 0 │ True │ 1 │ -1 │ False │
|
884
|
-
│ 1 │ True │ 2 │ -1 │ False │
|
885
|
-
│ 2 │ True │ 3 │ -1 │ False │
|
886
|
-
└───────────┴────────────┴────────┴──────────┴─────────────┘
|
887
|
-
"""
|
888
|
-
s = Survey()
|
889
|
-
for question in self.questions:
|
890
|
-
s.add_question(question)
|
891
|
-
return s
|
892
|
-
|
893
463
|
def add_skip_rule(
|
894
464
|
self, question: Union[QuestionBase, str], expression: str
|
895
465
|
) -> Survey:
|
@@ -917,6 +487,36 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
917
487
|
self._add_rule(question, expression, question_index + 1, before_rule=True)
|
918
488
|
return self
|
919
489
|
|
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
|
+
|
920
520
|
def _get_new_rule_priority(
|
921
521
|
self, question_index: int, before_rule: bool = False
|
922
522
|
) -> int:
|
@@ -1015,9 +615,9 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1015
615
|
|
1016
616
|
return self
|
1017
617
|
|
1018
|
-
|
1019
|
-
|
1020
|
-
|
618
|
+
###################
|
619
|
+
# FORWARD METHODS
|
620
|
+
###################
|
1021
621
|
def by(self, *args: Union["Agent", "Scenario", "LanguageModel"]) -> "Jobs":
|
1022
622
|
"""Add Agents, Scenarios, and LanguageModels to a survey and returns a runnable Jobs object.
|
1023
623
|
|
@@ -1040,63 +640,34 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1040
640
|
|
1041
641
|
return Jobs(survey=self)
|
1042
642
|
|
1043
|
-
# endregion
|
1044
|
-
|
1045
|
-
# region: Running the survey
|
1046
|
-
|
1047
|
-
def __call__(self, model=None, agent=None, cache=None, **kwargs):
|
1048
|
-
"""Run the survey with default model, taking the required survey as arguments.
|
1049
|
-
|
1050
|
-
>>> from edsl.questions import QuestionFunctional
|
1051
|
-
>>> def f(scenario, agent_traits): return "yes" if scenario["period"] == "morning" else "no"
|
1052
|
-
>>> q = QuestionFunctional(question_name = "q0", func = f)
|
1053
|
-
>>> s = Survey([q])
|
1054
|
-
>>> s(period = "morning", cache = False).select("answer.q0").first()
|
1055
|
-
'yes'
|
1056
|
-
>>> s(period = "evening", cache = False).select("answer.q0").first()
|
1057
|
-
'no'
|
1058
|
-
"""
|
1059
|
-
job = self.get_job(model, agent, **kwargs)
|
1060
|
-
return job.run(cache=cache)
|
1061
|
-
|
1062
|
-
async def run_async(self, model=None, agent=None, cache=None, **kwargs):
|
1063
|
-
"""Run the survey with default model, taking the required survey as arguments.
|
1064
|
-
|
1065
|
-
>>> from edsl.questions import QuestionFunctional
|
1066
|
-
>>> def f(scenario, agent_traits): return "yes" if scenario["period"] == "morning" else "no"
|
1067
|
-
>>> q = QuestionFunctional(question_name = "q0", func = f)
|
1068
|
-
>>> s = Survey([q])
|
1069
|
-
>>> s(period = "morning").select("answer.q0").first()
|
1070
|
-
'yes'
|
1071
|
-
>>> s(period = "evening").select("answer.q0").first()
|
1072
|
-
'no'
|
1073
|
-
"""
|
1074
|
-
# TODO: temp fix by creating a cache
|
1075
|
-
if cache is None:
|
1076
|
-
from edsl.data import Cache
|
1077
|
-
|
1078
|
-
c = Cache()
|
1079
|
-
else:
|
1080
|
-
c = cache
|
1081
|
-
jobs: "Jobs" = self.get_job(model, agent, **kwargs)
|
1082
|
-
return await jobs.run_async(cache=c)
|
1083
|
-
|
1084
643
|
def run(self, *args, **kwargs) -> "Results":
|
1085
644
|
"""Turn the survey into a Job and runs it.
|
1086
645
|
|
646
|
+
Here we run a survey but with debug mode on (so LLM calls are not made)
|
647
|
+
|
1087
648
|
>>> from edsl import QuestionFreeText
|
1088
649
|
>>> s = Survey([QuestionFreeText.example()])
|
1089
|
-
>>>
|
1090
|
-
>>>
|
1091
|
-
|
1092
|
-
|
1093
|
-
|
650
|
+
>>> results = s.run(debug = True, cache = False)
|
651
|
+
>>> results.select('answer.*').print(format = "rich")
|
652
|
+
┏━━━━━━━━━━━━━━┓
|
653
|
+
┃ answer ┃
|
654
|
+
┃ .how_are_you ┃
|
655
|
+
┡━━━━━━━━━━━━━━┩
|
656
|
+
...
|
657
|
+
└──────────────┘
|
1094
658
|
"""
|
1095
659
|
from edsl.jobs.Jobs import Jobs
|
1096
660
|
|
1097
661
|
return Jobs(survey=self).run(*args, **kwargs)
|
1098
662
|
|
1099
|
-
|
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
|
+
|
1100
671
|
def next_question(
|
1101
672
|
self, current_question: Union[str, QuestionBase], answers: dict
|
1102
673
|
) -> Union[QuestionBase, EndOfSurvey.__class__]:
|
@@ -1174,15 +745,9 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1174
745
|
Question('multiple_choice', question_name = \"""q0\""", question_text = \"""Do you like school?\""", question_options = ['yes', 'no'])
|
1175
746
|
>>> i2.send({"q0": "no"})
|
1176
747
|
Question('multiple_choice', question_name = \"""q1\""", question_text = \"""Why not?\""", question_options = ['killer bees in cafeteria', 'other'])
|
1177
|
-
|
1178
|
-
|
1179
748
|
"""
|
1180
749
|
self.answers = {}
|
1181
|
-
question = self.
|
1182
|
-
# should the first question be skipped?
|
1183
|
-
if self.rule_collection.skip_question_before_running(0, self.answers):
|
1184
|
-
question = self.next_question(question, self.answers)
|
1185
|
-
|
750
|
+
question = self._first_question()
|
1186
751
|
while not question == EndOfSurvey:
|
1187
752
|
# breakpoint()
|
1188
753
|
answer = yield question
|
@@ -1191,9 +756,34 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1191
756
|
## TODO: This should also include survey and agent attributes
|
1192
757
|
question = self.next_question(question, self.answers)
|
1193
758
|
|
1194
|
-
|
759
|
+
@property
|
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
|
1195
786
|
|
1196
|
-
# regions: DAG construction
|
1197
787
|
def textify(self, index_dag: DAG) -> DAG:
|
1198
788
|
"""Convert the DAG of question indices to a DAG of question names.
|
1199
789
|
|
@@ -1333,6 +923,59 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1333
923
|
return False
|
1334
924
|
return self.to_dict() == other.to_dict()
|
1335
925
|
|
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
|
+
|
1336
979
|
@classmethod
|
1337
980
|
def from_qsf(
|
1338
981
|
cls, qsf_file: Optional[str] = None, url: Optional[str] = None
|
@@ -1359,7 +1002,9 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1359
1002
|
so = SurveyQualtricsImport(qsf_file)
|
1360
1003
|
return so.create_survey()
|
1361
1004
|
|
1362
|
-
|
1005
|
+
###################
|
1006
|
+
# DISPLAY METHODS
|
1007
|
+
###################
|
1363
1008
|
def print(self):
|
1364
1009
|
"""Print the survey in a rich format.
|
1365
1010
|
|
@@ -1378,8 +1023,7 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1378
1023
|
def __repr__(self) -> str:
|
1379
1024
|
"""Return a string representation of the survey."""
|
1380
1025
|
|
1381
|
-
|
1382
|
-
questions_string = ", ".join([repr(q) for q in self.raw_passed_questions or []])
|
1026
|
+
questions_string = ", ".join([repr(q) for q in self._questions])
|
1383
1027
|
# question_names_string = ", ".join([repr(name) for name in self.question_names])
|
1384
1028
|
return f"Survey(questions=[{questions_string}], memory_plan={self.memory_plan}, rule_collection={self.rule_collection}, question_groups={self.question_groups})"
|
1385
1029
|
|
@@ -1388,6 +1032,22 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1388
1032
|
|
1389
1033
|
return data_to_html(self.to_dict())
|
1390
1034
|
|
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
|
+
|
1391
1051
|
def rich_print(self) -> Table:
|
1392
1052
|
"""Print the survey in a rich format.
|
1393
1053
|
|
@@ -1423,8 +1083,6 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1423
1083
|
|
1424
1084
|
return table
|
1425
1085
|
|
1426
|
-
# endregion
|
1427
|
-
|
1428
1086
|
def codebook(self) -> dict[str, str]:
|
1429
1087
|
"""Create a codebook for the survey, mapping question names to question text.
|
1430
1088
|
|
@@ -1437,7 +1095,6 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1437
1095
|
codebook[question.question_name] = question.question_text
|
1438
1096
|
return codebook
|
1439
1097
|
|
1440
|
-
# region: Export methods
|
1441
1098
|
def to_csv(self, filename: str = None):
|
1442
1099
|
"""Export the survey to a CSV file.
|
1443
1100
|
|
@@ -1480,16 +1137,8 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1480
1137
|
res = c.web(self.to_dict(), platform, email)
|
1481
1138
|
return res
|
1482
1139
|
|
1483
|
-
# endregion
|
1484
|
-
|
1485
1140
|
@classmethod
|
1486
|
-
def example(
|
1487
|
-
cls,
|
1488
|
-
params: bool = False,
|
1489
|
-
randomize: bool = False,
|
1490
|
-
include_instructions=False,
|
1491
|
-
custom_instructions: Optional[str] = None,
|
1492
|
-
) -> Survey:
|
1141
|
+
def example(cls, params: bool = False, randomize: bool = False) -> Survey:
|
1493
1142
|
"""Return an example survey.
|
1494
1143
|
|
1495
1144
|
>>> s = Survey.example()
|
@@ -1522,18 +1171,6 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1522
1171
|
)
|
1523
1172
|
s = cls(questions=[q0, q1, q2, q3])
|
1524
1173
|
return s
|
1525
|
-
|
1526
|
-
if include_instructions:
|
1527
|
-
from edsl import Instruction
|
1528
|
-
|
1529
|
-
custom_instructions = (
|
1530
|
-
custom_instructions if custom_instructions else "Please pay attention!"
|
1531
|
-
)
|
1532
|
-
|
1533
|
-
i = Instruction(text=custom_instructions, name="attention")
|
1534
|
-
s = cls(questions=[i, q0, q1, q2])
|
1535
|
-
return s
|
1536
|
-
|
1537
1174
|
s = cls(questions=[q0, q1, q2])
|
1538
1175
|
s = s.add_rule(q0, "q0 == 'yes'", q2)
|
1539
1176
|
return s
|
@@ -1555,6 +1192,43 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1555
1192
|
|
1556
1193
|
return self.by(s).by(agent).by(model)
|
1557
1194
|
|
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
|
+
|
1558
1232
|
|
1559
1233
|
def main():
|
1560
1234
|
"""Run the example survey."""
|