edsl 0.1.31.dev4__py3-none-any.whl → 0.1.33__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 +9 -3
- edsl/TemplateLoader.py +24 -0
- edsl/__init__.py +8 -3
- edsl/__version__.py +1 -1
- edsl/agents/Agent.py +40 -8
- edsl/agents/AgentList.py +43 -0
- edsl/agents/Invigilator.py +136 -221
- edsl/agents/InvigilatorBase.py +148 -59
- edsl/agents/{PromptConstructionMixin.py → PromptConstructor.py} +154 -85
- 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 +48 -47
- edsl/conjure/Conjure.py +6 -0
- edsl/coop/PriceFetcher.py +58 -0
- edsl/coop/coop.py +50 -7
- edsl/data/Cache.py +35 -1
- edsl/data/CacheHandler.py +3 -4
- edsl/data_transfer_models.py +73 -38
- edsl/enums.py +8 -0
- edsl/exceptions/general.py +10 -8
- 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 +112 -0
- edsl/inference_services/AzureAI.py +214 -0
- edsl/inference_services/DeepInfraService.py +4 -3
- edsl/inference_services/GoogleService.py +16 -12
- edsl/inference_services/GroqService.py +5 -4
- edsl/inference_services/InferenceServiceABC.py +58 -3
- edsl/inference_services/InferenceServicesCollection.py +13 -8
- edsl/inference_services/MistralAIService.py +120 -0
- edsl/inference_services/OllamaService.py +18 -0
- edsl/inference_services/OpenAIService.py +55 -56
- edsl/inference_services/TestService.py +80 -0
- edsl/inference_services/TogetherAIService.py +170 -0
- edsl/inference_services/models_available_cache.py +25 -0
- edsl/inference_services/registry.py +19 -1
- edsl/jobs/Answers.py +10 -12
- edsl/jobs/FailedQuestion.py +78 -0
- edsl/jobs/Jobs.py +137 -41
- edsl/jobs/buckets/BucketCollection.py +24 -15
- edsl/jobs/buckets/TokenBucket.py +105 -18
- edsl/jobs/interviews/Interview.py +393 -83
- edsl/jobs/interviews/{interview_exception_tracking.py → InterviewExceptionCollection.py} +22 -18
- edsl/jobs/interviews/InterviewExceptionEntry.py +167 -0
- edsl/jobs/runners/JobsRunnerAsyncio.py +152 -160
- edsl/jobs/runners/JobsRunnerStatus.py +331 -0
- edsl/jobs/tasks/QuestionTaskCreator.py +30 -23
- edsl/jobs/tasks/TaskCreators.py +1 -1
- edsl/jobs/tasks/TaskHistory.py +205 -126
- edsl/language_models/LanguageModel.py +297 -177
- 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 +25 -8
- edsl/language_models/repair.py +0 -19
- edsl/language_models/utilities.py +61 -0
- edsl/notebooks/Notebook.py +20 -2
- edsl/prompts/Prompt.py +52 -2
- edsl/questions/AnswerValidatorMixin.py +23 -26
- edsl/questions/QuestionBase.py +330 -249
- edsl/questions/QuestionBaseGenMixin.py +133 -0
- edsl/questions/QuestionBasePromptsMixin.py +266 -0
- edsl/questions/QuestionBudget.py +99 -42
- edsl/questions/QuestionCheckBox.py +227 -36
- edsl/questions/QuestionExtract.py +98 -28
- edsl/questions/QuestionFreeText.py +47 -31
- edsl/questions/QuestionFunctional.py +7 -0
- edsl/questions/QuestionList.py +141 -23
- edsl/questions/QuestionMultipleChoice.py +159 -66
- edsl/questions/QuestionNumerical.py +88 -47
- edsl/questions/QuestionRank.py +182 -25
- edsl/questions/Quick.py +41 -0
- edsl/questions/RegisterQuestionsMeta.py +31 -12
- edsl/questions/ResponseValidatorABC.py +170 -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 +15 -2
- edsl/questions/derived/QuestionTopK.py +10 -1
- edsl/questions/derived/QuestionYesNo.py +24 -3
- 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/budget/__init__.py +0 -0
- edsl/questions/templates/budget/answering_instructions.jinja +7 -0
- edsl/questions/templates/budget/question_presentation.jinja +7 -0
- edsl/questions/templates/checkbox/__init__.py +0 -0
- edsl/questions/templates/checkbox/answering_instructions.jinja +10 -0
- edsl/questions/templates/checkbox/question_presentation.jinja +22 -0
- edsl/questions/templates/extract/__init__.py +0 -0
- edsl/questions/templates/extract/answering_instructions.jinja +7 -0
- edsl/questions/templates/extract/question_presentation.jinja +1 -0
- edsl/questions/templates/free_text/__init__.py +0 -0
- edsl/questions/templates/free_text/answering_instructions.jinja +0 -0
- edsl/questions/templates/free_text/question_presentation.jinja +1 -0
- edsl/questions/templates/likert_five/__init__.py +0 -0
- edsl/questions/templates/likert_five/answering_instructions.jinja +10 -0
- edsl/questions/templates/likert_five/question_presentation.jinja +12 -0
- edsl/questions/templates/linear_scale/__init__.py +0 -0
- edsl/questions/templates/linear_scale/answering_instructions.jinja +5 -0
- edsl/questions/templates/linear_scale/question_presentation.jinja +5 -0
- edsl/questions/templates/list/__init__.py +0 -0
- edsl/questions/templates/list/answering_instructions.jinja +4 -0
- edsl/questions/templates/list/question_presentation.jinja +5 -0
- edsl/questions/templates/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/__init__.py +0 -0
- edsl/questions/templates/rank/answering_instructions.jinja +11 -0
- edsl/questions/templates/rank/question_presentation.jinja +15 -0
- edsl/questions/templates/top_k/__init__.py +0 -0
- edsl/questions/templates/top_k/answering_instructions.jinja +8 -0
- edsl/questions/templates/top_k/question_presentation.jinja +22 -0
- edsl/questions/templates/yes_no/__init__.py +0 -0
- edsl/questions/templates/yes_no/answering_instructions.jinja +6 -0
- edsl/questions/templates/yes_no/question_presentation.jinja +12 -0
- edsl/results/Dataset.py +20 -0
- edsl/results/DatasetExportMixin.py +58 -30
- edsl/results/DatasetTree.py +145 -0
- edsl/results/Result.py +32 -5
- edsl/results/Results.py +135 -46
- edsl/results/ResultsDBMixin.py +3 -3
- edsl/results/Selector.py +118 -0
- edsl/results/tree_explore.py +115 -0
- edsl/scenarios/FileStore.py +71 -10
- edsl/scenarios/Scenario.py +109 -24
- edsl/scenarios/ScenarioImageMixin.py +2 -2
- edsl/scenarios/ScenarioList.py +546 -21
- edsl/scenarios/ScenarioListExportMixin.py +24 -4
- edsl/scenarios/ScenarioListPdfMixin.py +153 -4
- edsl/study/SnapShot.py +8 -1
- edsl/study/Study.py +32 -0
- edsl/surveys/Rule.py +15 -3
- edsl/surveys/RuleCollection.py +21 -5
- edsl/surveys/Survey.py +707 -298
- edsl/surveys/SurveyExportMixin.py +71 -9
- edsl/surveys/SurveyFlowVisualizationMixin.py +2 -1
- edsl/surveys/SurveyQualtricsImport.py +284 -0
- 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 +116 -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/utilities/utilities.py +40 -1
- {edsl-0.1.31.dev4.dist-info → edsl-0.1.33.dist-info}/METADATA +8 -2
- edsl-0.1.33.dist-info/RECORD +295 -0
- edsl/jobs/interviews/InterviewTaskBuildingMixin.py +0 -271
- edsl/jobs/interviews/retry_management.py +0 -37
- edsl/jobs/runners/JobsRunnerStatusMixin.py +0 -303
- edsl/utilities/gcp_bucket/simple_example.py +0 -9
- edsl-0.1.31.dev4.dist-info/RECORD +0 -204
- {edsl-0.1.31.dev4.dist-info → edsl-0.1.33.dist-info}/LICENSE +0 -0
- {edsl-0.1.31.dev4.dist-info → edsl-0.1.33.dist-info}/WHEEL +0 -0
edsl/surveys/Survey.py
CHANGED
@@ -2,6 +2,9 @@
|
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
import re
|
5
|
+
import tempfile
|
6
|
+
import requests
|
7
|
+
|
5
8
|
from typing import Any, Generator, Optional, Union, List, Literal, Callable
|
6
9
|
from uuid import uuid4
|
7
10
|
from edsl.Base import Base
|
@@ -17,6 +20,24 @@ from edsl.surveys.SurveyExportMixin import SurveyExportMixin
|
|
17
20
|
from edsl.surveys.SurveyFlowVisualizationMixin import SurveyFlowVisualizationMixin
|
18
21
|
from edsl.utilities.decorators import add_edsl_version, remove_edsl_version
|
19
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
|
+
|
20
41
|
|
21
42
|
class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
22
43
|
"""A collection of questions that supports skip logic."""
|
@@ -39,11 +60,13 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
39
60
|
|
40
61
|
def __init__(
|
41
62
|
self,
|
42
|
-
questions: Optional[
|
63
|
+
questions: Optional[
|
64
|
+
list[Union[QuestionBase, Instruction, ChangeInstruction]]
|
65
|
+
] = None,
|
43
66
|
memory_plan: Optional[MemoryPlan] = None,
|
44
67
|
rule_collection: Optional[RuleCollection] = None,
|
45
68
|
question_groups: Optional[dict[str, tuple[int, int]]] = None,
|
46
|
-
name: str = None,
|
69
|
+
name: Optional[str] = None,
|
47
70
|
):
|
48
71
|
"""Create a new survey.
|
49
72
|
|
@@ -53,13 +76,33 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
53
76
|
:param question_groups: The groups of questions in the survey.
|
54
77
|
:param name: The name of the survey - DEPRECATED.
|
55
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
|
+
|
56
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
|
+
|
57
97
|
self.rule_collection = RuleCollection(
|
58
|
-
num_questions=len(
|
98
|
+
num_questions=len(true_questions) if true_questions else None
|
59
99
|
)
|
60
100
|
# the RuleCollection needs to be present while we add the questions; we might override this later
|
61
101
|
# if a rule_collection is provided. This allows us to serialize the survey with the rule_collection.
|
62
|
-
|
102
|
+
|
103
|
+
self.questions = true_questions
|
104
|
+
self.instruction_names_to_instructions = instruction_names_to_instructions
|
105
|
+
|
63
106
|
self.memory_plan = memory_plan or MemoryPlan(self)
|
64
107
|
if question_groups is not None:
|
65
108
|
self.question_groups = question_groups
|
@@ -75,6 +118,241 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
75
118
|
|
76
119
|
warnings.warn("name parameter to a survey is deprecated.")
|
77
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
|
+
|
292
|
+
def simulate(self) -> dict:
|
293
|
+
"""Simulate the survey and return the answers."""
|
294
|
+
i = self.gen_path_through_survey()
|
295
|
+
q = next(i)
|
296
|
+
while True:
|
297
|
+
try:
|
298
|
+
answer = q._simulate_answer()
|
299
|
+
q = i.send({q.question_name: answer["answer"]})
|
300
|
+
except StopIteration:
|
301
|
+
break
|
302
|
+
return self.answers
|
303
|
+
|
304
|
+
def create_agent(self) -> "Agent":
|
305
|
+
"""Create an agent from the simulated answers."""
|
306
|
+
answers_dict = self.simulate()
|
307
|
+
|
308
|
+
def construct_answer_dict_function(traits: dict) -> Callable:
|
309
|
+
def func(self, question: "QuestionBase", scenario=None):
|
310
|
+
return traits.get(question.question_name, None)
|
311
|
+
|
312
|
+
return func
|
313
|
+
|
314
|
+
return Agent(traits=answers_dict).add_direct_question_answering_method(
|
315
|
+
construct_answer_dict_function(answers_dict)
|
316
|
+
)
|
317
|
+
|
318
|
+
def simulate_results(self) -> "Results":
|
319
|
+
"""Simulate the survey and return the results."""
|
320
|
+
a = self.create_agent()
|
321
|
+
return self.by([a]).run()
|
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
|
+
|
78
356
|
def get(self, question_name: str) -> QuestionBase:
|
79
357
|
"""
|
80
358
|
Return the question object given the question name.
|
@@ -90,22 +368,184 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
90
368
|
index = self.question_name_to_index[question_name]
|
91
369
|
return self._questions[index]
|
92
370
|
|
93
|
-
def question_names_to_questions(self) -> dict:
|
94
|
-
"""Return a dictionary mapping question names to question attributes."""
|
95
|
-
return {q.question_name: q for q in self.questions}
|
96
|
-
|
97
371
|
def get_question(self, question_name: str) -> QuestionBase:
|
98
372
|
"""Return the question object given the question name."""
|
99
373
|
# import warnings
|
100
374
|
# warnings.warn("survey.get_question is deprecated. Use subscript operator instead.")
|
101
375
|
return self.get(question_name)
|
102
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
|
103
409
|
def __hash__(self) -> int:
|
104
410
|
"""Return a hash of the question."""
|
105
411
|
from edsl.utilities.utilities import dict_hash
|
106
412
|
|
107
413
|
return dict_hash(self._to_dict())
|
108
414
|
|
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
|
109
549
|
def __add__(self, other: Survey) -> Survey:
|
110
550
|
"""Combine two surveys.
|
111
551
|
|
@@ -133,45 +573,6 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
133
573
|
|
134
574
|
return Survey(questions=self.questions + other.questions)
|
135
575
|
|
136
|
-
def clear_non_default_rules(self) -> Survey:
|
137
|
-
s = Survey()
|
138
|
-
for question in self.questions:
|
139
|
-
s.add_question(question)
|
140
|
-
return s
|
141
|
-
|
142
|
-
@property
|
143
|
-
def parameters(self):
|
144
|
-
return set.union(*[q.parameters for q in self.questions])
|
145
|
-
|
146
|
-
@property
|
147
|
-
def parameters_by_question(self):
|
148
|
-
return {q.question_name: q.parameters for q in self.questions}
|
149
|
-
|
150
|
-
@property
|
151
|
-
def question_names(self) -> list[str]:
|
152
|
-
"""Return a list of question names in the survey.
|
153
|
-
|
154
|
-
Example:
|
155
|
-
|
156
|
-
>>> s = Survey.example()
|
157
|
-
>>> s.question_names
|
158
|
-
['q0', 'q1', 'q2']
|
159
|
-
"""
|
160
|
-
# return list(self.question_name_to_index.keys())
|
161
|
-
return [q.question_name for q in self.questions]
|
162
|
-
|
163
|
-
@property
|
164
|
-
def question_name_to_index(self) -> dict[str, int]:
|
165
|
-
"""Return a dictionary mapping question names to question indices.
|
166
|
-
|
167
|
-
Example:
|
168
|
-
|
169
|
-
>>> s = Survey.example()
|
170
|
-
>>> s.question_name_to_index
|
171
|
-
{'q0': 0, 'q1': 1, 'q2': 2}
|
172
|
-
"""
|
173
|
-
return {q.question_name: i for i, q in enumerate(self.questions)}
|
174
|
-
|
175
576
|
def add_question(self, question: QuestionBase) -> Survey:
|
176
577
|
"""
|
177
578
|
Add a question to survey.
|
@@ -189,11 +590,11 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
189
590
|
>>> s = Survey().add_question(q).add_question(q)
|
190
591
|
Traceback (most recent call last):
|
191
592
|
...
|
192
|
-
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'].
|
193
594
|
"""
|
194
595
|
if question.question_name in self.question_names:
|
195
596
|
raise SurveyCreationError(
|
196
|
-
f"""Question name already exists in survey.
|
597
|
+
f"""Question name '{question.question_name}' already exists in survey. Existing names are {self.question_names}."""
|
197
598
|
)
|
198
599
|
index = len(self.questions)
|
199
600
|
# TODO: This is a bit ugly because the user
|
@@ -201,6 +602,8 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
201
602
|
# descriptor.
|
202
603
|
self._questions.append(question)
|
203
604
|
|
605
|
+
self.pseudo_indices[question.question_name] = index
|
606
|
+
|
204
607
|
# using index + 1 presumes there is a next question
|
205
608
|
self.rule_collection.add_rule(
|
206
609
|
Rule(
|
@@ -219,6 +622,20 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
219
622
|
|
220
623
|
return self
|
221
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
|
222
639
|
def set_full_memory_mode(self) -> Survey:
|
223
640
|
"""Add instructions to a survey that the agent should remember all of the answers to the questions in the survey.
|
224
641
|
|
@@ -251,6 +668,75 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
251
668
|
prior_questions=prior_questions_func(i),
|
252
669
|
)
|
253
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
|
254
740
|
def add_question_group(
|
255
741
|
self,
|
256
742
|
start_question: Union[QuestionBase, str],
|
@@ -325,73 +811,28 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
325
811
|
if start_index < existing_end_index and end_index > existing_end_index:
|
326
812
|
raise ValueError(f"Group {group_name} overlaps with the new group.")
|
327
813
|
|
328
|
-
self.question_groups[group_name] = (start_index, end_index)
|
329
|
-
return self
|
330
|
-
|
331
|
-
def add_targeted_memory(
|
332
|
-
self,
|
333
|
-
focal_question: Union[QuestionBase, str],
|
334
|
-
prior_question: Union[QuestionBase, str],
|
335
|
-
) -> Survey:
|
336
|
-
"""Add instructions to a survey than when answering focal_question.
|
337
|
-
|
338
|
-
:param focal_question: The question that the agent is answering.
|
339
|
-
:param prior_question: The question that the agent should remember when answering the focal question.
|
340
|
-
|
341
|
-
Here we add instructions to a survey than when answering q2 they should remember q1:
|
342
|
-
|
343
|
-
>>> s = Survey.example().add_targeted_memory("q2", "q0")
|
344
|
-
>>> s.memory_plan
|
345
|
-
{'q2': Memory(prior_questions=['q0'])}
|
346
|
-
|
347
|
-
The agent should also remember the answers to prior_questions listed in prior_questions.
|
348
|
-
"""
|
349
|
-
focal_question_name = self.question_names[
|
350
|
-
self._get_question_index(focal_question)
|
351
|
-
]
|
352
|
-
prior_question_name = self.question_names[
|
353
|
-
self._get_question_index(prior_question)
|
354
|
-
]
|
355
|
-
|
356
|
-
self.memory_plan.add_single_memory(
|
357
|
-
focal_question=focal_question_name,
|
358
|
-
prior_question=prior_question_name,
|
359
|
-
)
|
360
|
-
|
361
|
-
return self
|
362
|
-
|
363
|
-
def add_memory_collection(
|
364
|
-
self,
|
365
|
-
focal_question: Union[QuestionBase, str],
|
366
|
-
prior_questions: List[Union[QuestionBase, str]],
|
367
|
-
) -> Survey:
|
368
|
-
"""Add prior questions and responses so the agent has them when answering.
|
369
|
-
|
370
|
-
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.
|
371
|
-
|
372
|
-
:param focal_question: The question that the agent is answering.
|
373
|
-
:param prior_questions: The questions that the agent should remember when answering the focal question.
|
374
|
-
|
375
|
-
Here we have it so that when answering q2, the agent should remember answers to q0 and q1:
|
376
|
-
|
377
|
-
>>> s = Survey.example().add_memory_collection("q2", ["q0", "q1"])
|
378
|
-
>>> s.memory_plan
|
379
|
-
{'q2': Memory(prior_questions=['q0', 'q1'])}
|
380
|
-
"""
|
381
|
-
focal_question_name = self.question_names[
|
382
|
-
self._get_question_index(focal_question)
|
383
|
-
]
|
384
|
-
|
385
|
-
prior_question_names = [
|
386
|
-
self.question_names[self._get_question_index(prior_question)]
|
387
|
-
for prior_question in prior_questions
|
388
|
-
]
|
389
|
-
|
390
|
-
self.memory_plan.add_memory_collection(
|
391
|
-
focal_question=focal_question_name, prior_questions=prior_question_names
|
392
|
-
)
|
814
|
+
self.question_groups[group_name] = (start_index, end_index)
|
393
815
|
return self
|
394
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
|
+
|
395
836
|
def add_stop_rule(
|
396
837
|
self, question: Union[QuestionBase, str], expression: str
|
397
838
|
) -> Survey:
|
@@ -400,6 +841,7 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
400
841
|
:param question: The question to add the stop rule to.
|
401
842
|
:param expression: The expression to evaluate.
|
402
843
|
|
844
|
+
If this rule is true, the survey ends.
|
403
845
|
The rule is evaluated *after* the question is answered. If the rule is true, the survey ends.
|
404
846
|
|
405
847
|
Here, answering "yes" to q0 ends the survey:
|
@@ -412,10 +854,42 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
412
854
|
|
413
855
|
>>> s.next_question("q0", {"q0": "no"}).question_name
|
414
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 '!='.
|
415
862
|
"""
|
863
|
+
expression = ValidatedString(expression)
|
416
864
|
self.add_rule(question, expression, EndOfSurvey)
|
417
865
|
return self
|
418
866
|
|
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
|
+
|
419
893
|
def add_skip_rule(
|
420
894
|
self, question: Union[QuestionBase, str], expression: str
|
421
895
|
) -> Survey:
|
@@ -443,36 +917,6 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
443
917
|
self._add_rule(question, expression, question_index + 1, before_rule=True)
|
444
918
|
return self
|
445
919
|
|
446
|
-
def _get_question_index(
|
447
|
-
self, q: Union[QuestionBase, str, EndOfSurvey.__class__]
|
448
|
-
) -> Union[int, EndOfSurvey.__class__]:
|
449
|
-
"""Return the index of the question or EndOfSurvey object.
|
450
|
-
|
451
|
-
:param q: The question or question name to get the index of.
|
452
|
-
|
453
|
-
It can handle it if the user passes in the question name, the question object, or the EndOfSurvey object.
|
454
|
-
|
455
|
-
>>> s = Survey.example()
|
456
|
-
>>> s._get_question_index("q0")
|
457
|
-
0
|
458
|
-
|
459
|
-
This doesnt' work with questions that don't exist:
|
460
|
-
|
461
|
-
>>> s._get_question_index("poop")
|
462
|
-
Traceback (most recent call last):
|
463
|
-
...
|
464
|
-
ValueError: Question name poop not found in survey. The current question names are {'q0': 0, 'q1': 1, 'q2': 2}.
|
465
|
-
"""
|
466
|
-
if q == EndOfSurvey:
|
467
|
-
return EndOfSurvey
|
468
|
-
else:
|
469
|
-
question_name = q if isinstance(q, str) else q.question_name
|
470
|
-
if question_name not in self.question_name_to_index:
|
471
|
-
raise ValueError(
|
472
|
-
f"""Question name {question_name} not found in survey. The current question names are {self.question_name_to_index}."""
|
473
|
-
)
|
474
|
-
return self.question_name_to_index[question_name]
|
475
|
-
|
476
920
|
def _get_new_rule_priority(
|
477
921
|
self, question_index: int, before_rule: bool = False
|
478
922
|
) -> int:
|
@@ -571,9 +1015,9 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
571
1015
|
|
572
1016
|
return self
|
573
1017
|
|
574
|
-
|
575
|
-
|
576
|
-
|
1018
|
+
# endregion
|
1019
|
+
|
1020
|
+
# region: Forward methods
|
577
1021
|
def by(self, *args: Union["Agent", "Scenario", "LanguageModel"]) -> "Jobs":
|
578
1022
|
"""Add Agents, Scenarios, and LanguageModels to a survey and returns a runnable Jobs object.
|
579
1023
|
|
@@ -596,34 +1040,63 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
596
1040
|
|
597
1041
|
return Jobs(survey=self)
|
598
1042
|
|
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
|
+
|
599
1084
|
def run(self, *args, **kwargs) -> "Results":
|
600
1085
|
"""Turn the survey into a Job and runs it.
|
601
1086
|
|
602
|
-
Here we run a survey but with debug mode on (so LLM calls are not made)
|
603
|
-
|
604
1087
|
>>> from edsl import QuestionFreeText
|
605
1088
|
>>> s = Survey([QuestionFreeText.example()])
|
606
|
-
>>>
|
607
|
-
>>>
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
┡━━━━━━━━━━━━━━┩
|
612
|
-
...
|
613
|
-
└──────────────┘
|
1089
|
+
>>> from edsl.language_models import LanguageModel
|
1090
|
+
>>> m = LanguageModel.example(test_model = True, canned_response = "Great!")
|
1091
|
+
>>> results = s.by(m).run(cache = False)
|
1092
|
+
>>> results.select('answer.*')
|
1093
|
+
Dataset([{'answer.how_are_you': ['Great!']}])
|
614
1094
|
"""
|
615
1095
|
from edsl.jobs.Jobs import Jobs
|
616
1096
|
|
617
1097
|
return Jobs(survey=self).run(*args, **kwargs)
|
618
1098
|
|
619
|
-
|
620
|
-
## Survey-Taking Methods
|
621
|
-
########################
|
622
|
-
|
623
|
-
def _first_question(self) -> QuestionBase:
|
624
|
-
"""Return the first question in the survey."""
|
625
|
-
return self.questions[0]
|
626
|
-
|
1099
|
+
# region: Survey flow
|
627
1100
|
def next_question(
|
628
1101
|
self, current_question: Union[str, QuestionBase], answers: dict
|
629
1102
|
) -> Union[QuestionBase, EndOfSurvey.__class__]:
|
@@ -701,41 +1174,26 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
701
1174
|
Question('multiple_choice', question_name = \"""q0\""", question_text = \"""Do you like school?\""", question_options = ['yes', 'no'])
|
702
1175
|
>>> i2.send({"q0": "no"})
|
703
1176
|
Question('multiple_choice', question_name = \"""q1\""", question_text = \"""Why not?\""", question_options = ['killer bees in cafeteria', 'other'])
|
1177
|
+
|
1178
|
+
|
704
1179
|
"""
|
705
|
-
|
1180
|
+
self.answers = {}
|
1181
|
+
question = self._questions[0]
|
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
|
+
|
706
1186
|
while not question == EndOfSurvey:
|
707
|
-
|
1187
|
+
# breakpoint()
|
1188
|
+
answer = yield question
|
1189
|
+
self.answers.update(answer)
|
1190
|
+
# print(f"Answers: {self.answers}")
|
708
1191
|
## TODO: This should also include survey and agent attributes
|
709
1192
|
question = self.next_question(question, self.answers)
|
710
1193
|
|
711
|
-
|
712
|
-
def scenario_attributes(self) -> list[str]:
|
713
|
-
"""Return a list of attributes that admissible Scenarios should have.
|
714
|
-
|
715
|
-
Here we have a survey with a question that uses a jinja2 style {{ }} template:
|
716
|
-
|
717
|
-
>>> from edsl import QuestionFreeText
|
718
|
-
>>> s = Survey().add_question(QuestionFreeText(question_text="{{ greeting }}. What is your name?", question_name="name"))
|
719
|
-
>>> s.scenario_attributes
|
720
|
-
['greeting']
|
721
|
-
|
722
|
-
>>> s = Survey().add_question(QuestionFreeText(question_text="{{ greeting }}. What is your {{ attribute }}?", question_name="name"))
|
723
|
-
>>> s.scenario_attributes
|
724
|
-
['greeting', 'attribute']
|
725
|
-
|
726
|
-
|
727
|
-
"""
|
728
|
-
temp = []
|
729
|
-
for question in self.questions:
|
730
|
-
question_text = question.question_text
|
731
|
-
# extract the contents of all {{ }} in the question text using regex
|
732
|
-
matches = re.findall(r"\{\{(.+?)\}\}", question_text)
|
733
|
-
# remove whitespace
|
734
|
-
matches = [match.strip() for match in matches]
|
735
|
-
# add them to the temp list
|
736
|
-
temp.extend(matches)
|
737
|
-
return temp
|
1194
|
+
# endregion
|
738
1195
|
|
1196
|
+
# regions: DAG construction
|
739
1197
|
def textify(self, index_dag: DAG) -> DAG:
|
740
1198
|
"""Convert the DAG of question indices to a DAG of question names.
|
741
1199
|
|
@@ -775,6 +1233,15 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
775
1233
|
|
776
1234
|
@property
|
777
1235
|
def piping_dag(self) -> DAG:
|
1236
|
+
"""Figures out the DAG of piping dependencies.
|
1237
|
+
|
1238
|
+
>>> from edsl import QuestionFreeText
|
1239
|
+
>>> q0 = QuestionFreeText(question_text="Here is a question", question_name="q0")
|
1240
|
+
>>> q1 = QuestionFreeText(question_text="You previously answered {{ q0 }}---how do you feel now?", question_name="q1")
|
1241
|
+
>>> s = Survey([q0, q1])
|
1242
|
+
>>> s.piping_dag
|
1243
|
+
{1: {0}}
|
1244
|
+
"""
|
778
1245
|
d = {}
|
779
1246
|
for question_name, depenencies in self.parameters_by_question.items():
|
780
1247
|
if depenencies:
|
@@ -866,62 +1333,33 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
866
1333
|
return False
|
867
1334
|
return self.to_dict() == other.to_dict()
|
868
1335
|
|
869
|
-
|
870
|
-
|
871
|
-
|
872
|
-
|
873
|
-
|
874
|
-
"""Serialize the Survey object to a dictionary.
|
875
|
-
|
876
|
-
>>> s = Survey.example()
|
877
|
-
>>> s._to_dict().keys()
|
878
|
-
dict_keys(['questions', 'memory_plan', 'rule_collection', 'question_groups'])
|
879
|
-
|
880
|
-
"""
|
881
|
-
return {
|
882
|
-
"questions": [q._to_dict() for q in self._questions],
|
883
|
-
"memory_plan": self.memory_plan.to_dict(),
|
884
|
-
"rule_collection": self.rule_collection.to_dict(),
|
885
|
-
"question_groups": self.question_groups,
|
886
|
-
}
|
887
|
-
|
888
|
-
@add_edsl_version
|
889
|
-
def to_dict(self) -> dict[str, Any]:
|
890
|
-
"""Serialize the Survey object to a dictionary.
|
1336
|
+
@classmethod
|
1337
|
+
def from_qsf(
|
1338
|
+
cls, qsf_file: Optional[str] = None, url: Optional[str] = None
|
1339
|
+
) -> Survey:
|
1340
|
+
"""Create a Survey object from a Qualtrics QSF file."""
|
891
1341
|
|
892
|
-
|
893
|
-
|
894
|
-
dict_keys(['questions', 'memory_plan', 'rule_collection', 'question_groups', 'edsl_version', 'edsl_class_name'])
|
1342
|
+
if url and qsf_file:
|
1343
|
+
raise ValueError("Only one of url or qsf_file can be provided.")
|
895
1344
|
|
896
|
-
|
897
|
-
|
1345
|
+
if (not url) and (not qsf_file):
|
1346
|
+
raise ValueError("Either url or qsf_file must be provided.")
|
898
1347
|
|
899
|
-
|
900
|
-
|
901
|
-
|
902
|
-
"""Deserialize the dictionary back to a Survey object.
|
1348
|
+
if url:
|
1349
|
+
response = requests.get(url)
|
1350
|
+
response.raise_for_status() # Ensure the request was successful
|
903
1351
|
|
904
|
-
|
1352
|
+
# Save the Excel file to a temporary file
|
1353
|
+
with tempfile.NamedTemporaryFile(suffix=".qsf", delete=False) as temp_file:
|
1354
|
+
temp_file.write(response.content)
|
1355
|
+
qsf_file = temp_file.name
|
905
1356
|
|
906
|
-
|
907
|
-
>>> s = Survey.from_dict(d)
|
908
|
-
>>> s == Survey.example()
|
909
|
-
True
|
1357
|
+
from edsl.surveys.SurveyQualtricsImport import SurveyQualtricsImport
|
910
1358
|
|
911
|
-
|
912
|
-
|
913
|
-
memory_plan = MemoryPlan.from_dict(data["memory_plan"])
|
914
|
-
survey = cls(
|
915
|
-
questions=questions,
|
916
|
-
memory_plan=memory_plan,
|
917
|
-
rule_collection=RuleCollection.from_dict(data["rule_collection"]),
|
918
|
-
question_groups=data["question_groups"],
|
919
|
-
)
|
920
|
-
return survey
|
1359
|
+
so = SurveyQualtricsImport(qsf_file)
|
1360
|
+
return so.create_survey()
|
921
1361
|
|
922
|
-
|
923
|
-
# DISPLAY METHODS
|
924
|
-
###################
|
1362
|
+
# region: Display methods
|
925
1363
|
def print(self):
|
926
1364
|
"""Print the survey in a rich format.
|
927
1365
|
|
@@ -940,7 +1378,8 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
940
1378
|
def __repr__(self) -> str:
|
941
1379
|
"""Return a string representation of the survey."""
|
942
1380
|
|
943
|
-
questions_string = ", ".join([repr(q) for q in self._questions])
|
1381
|
+
# questions_string = ", ".join([repr(q) for q in self._questions])
|
1382
|
+
questions_string = ", ".join([repr(q) for q in self.raw_passed_questions or []])
|
944
1383
|
# question_names_string = ", ".join([repr(name) for name in self.question_names])
|
945
1384
|
return f"Survey(questions=[{questions_string}], memory_plan={self.memory_plan}, rule_collection={self.rule_collection}, question_groups={self.question_groups})"
|
946
1385
|
|
@@ -949,22 +1388,6 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
949
1388
|
|
950
1389
|
return data_to_html(self.to_dict())
|
951
1390
|
|
952
|
-
def show_rules(self) -> None:
|
953
|
-
"""Print out the rules in the survey.
|
954
|
-
|
955
|
-
>>> s = Survey.example()
|
956
|
-
>>> s.show_rules()
|
957
|
-
┏━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━┓
|
958
|
-
┃ current_q ┃ expression ┃ next_q ┃ priority ┃ before_rule ┃
|
959
|
-
┡━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━┩
|
960
|
-
│ 0 │ True │ 1 │ -1 │ False │
|
961
|
-
│ 0 │ q0 == 'yes' │ 2 │ 0 │ False │
|
962
|
-
│ 1 │ True │ 2 │ -1 │ False │
|
963
|
-
│ 2 │ True │ 3 │ -1 │ False │
|
964
|
-
└───────────┴─────────────┴────────┴──────────┴─────────────┘
|
965
|
-
"""
|
966
|
-
self.rule_collection.show_rules()
|
967
|
-
|
968
1391
|
def rich_print(self) -> Table:
|
969
1392
|
"""Print the survey in a rich format.
|
970
1393
|
|
@@ -1000,6 +1423,8 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1000
1423
|
|
1001
1424
|
return table
|
1002
1425
|
|
1426
|
+
# endregion
|
1427
|
+
|
1003
1428
|
def codebook(self) -> dict[str, str]:
|
1004
1429
|
"""Create a codebook for the survey, mapping question names to question text.
|
1005
1430
|
|
@@ -1012,6 +1437,7 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1012
1437
|
codebook[question.question_name] = question.question_text
|
1013
1438
|
return codebook
|
1014
1439
|
|
1440
|
+
# region: Export methods
|
1015
1441
|
def to_csv(self, filename: str = None):
|
1016
1442
|
"""Export the survey to a CSV file.
|
1017
1443
|
|
@@ -1054,8 +1480,16 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1054
1480
|
res = c.web(self.to_dict(), platform, email)
|
1055
1481
|
return res
|
1056
1482
|
|
1483
|
+
# endregion
|
1484
|
+
|
1057
1485
|
@classmethod
|
1058
|
-
def example(
|
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:
|
1059
1493
|
"""Return an example survey.
|
1060
1494
|
|
1061
1495
|
>>> s = Survey.example()
|
@@ -1088,6 +1522,18 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1088
1522
|
)
|
1089
1523
|
s = cls(questions=[q0, q1, q2, q3])
|
1090
1524
|
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
|
+
|
1091
1537
|
s = cls(questions=[q0, q1, q2])
|
1092
1538
|
s = s.add_rule(q0, "q0 == 'yes'", q2)
|
1093
1539
|
return s
|
@@ -1109,43 +1555,6 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
|
|
1109
1555
|
|
1110
1556
|
return self.by(s).by(agent).by(model)
|
1111
1557
|
|
1112
|
-
def __call__(self, model=None, agent=None, cache=None, **kwargs):
|
1113
|
-
"""Run the survey with default model, taking the required survey as arguments.
|
1114
|
-
|
1115
|
-
>>> from edsl.questions import QuestionFunctional
|
1116
|
-
>>> def f(scenario, agent_traits): return "yes" if scenario["period"] == "morning" else "no"
|
1117
|
-
>>> q = QuestionFunctional(question_name = "q0", func = f)
|
1118
|
-
>>> s = Survey([q])
|
1119
|
-
>>> s(period = "morning", cache = False).select("answer.q0").first()
|
1120
|
-
'yes'
|
1121
|
-
>>> s(period = "evening", cache = False).select("answer.q0").first()
|
1122
|
-
'no'
|
1123
|
-
"""
|
1124
|
-
job = self.get_job(model, agent, **kwargs)
|
1125
|
-
return job.run(cache=cache)
|
1126
|
-
|
1127
|
-
async def run_async(self, model=None, agent=None, cache=None, **kwargs):
|
1128
|
-
"""Run the survey with default model, taking the required survey as arguments.
|
1129
|
-
|
1130
|
-
>>> from edsl.questions import QuestionFunctional
|
1131
|
-
>>> def f(scenario, agent_traits): return "yes" if scenario["period"] == "morning" else "no"
|
1132
|
-
>>> q = QuestionFunctional(question_name = "q0", func = f)
|
1133
|
-
>>> s = Survey([q])
|
1134
|
-
>>> s(period = "morning").select("answer.q0").first()
|
1135
|
-
'yes'
|
1136
|
-
>>> s(period = "evening").select("answer.q0").first()
|
1137
|
-
'no'
|
1138
|
-
"""
|
1139
|
-
# TODO: temp fix by creating a cache
|
1140
|
-
if cache is None:
|
1141
|
-
from edsl.data import Cache
|
1142
|
-
|
1143
|
-
c = Cache()
|
1144
|
-
else:
|
1145
|
-
c = cache
|
1146
|
-
jobs: "Jobs" = self.get_job(model, agent, **kwargs)
|
1147
|
-
return await jobs.run_async(cache=c)
|
1148
|
-
|
1149
1558
|
|
1150
1559
|
def main():
|
1151
1560
|
"""Run the example survey."""
|