edsl 0.1.38.dev4__py3-none-any.whl → 0.1.39__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- edsl/Base.py +197 -116
- edsl/__init__.py +15 -7
- edsl/__version__.py +1 -1
- edsl/agents/Agent.py +351 -147
- edsl/agents/AgentList.py +211 -73
- edsl/agents/Invigilator.py +101 -50
- edsl/agents/InvigilatorBase.py +62 -70
- edsl/agents/PromptConstructor.py +143 -225
- edsl/agents/QuestionInstructionPromptBuilder.py +128 -0
- edsl/agents/QuestionTemplateReplacementsBuilder.py +137 -0
- edsl/agents/__init__.py +0 -1
- edsl/agents/prompt_helpers.py +3 -3
- edsl/agents/question_option_processor.py +172 -0
- edsl/auto/AutoStudy.py +18 -5
- edsl/auto/StageBase.py +53 -40
- edsl/auto/StageQuestions.py +2 -1
- edsl/auto/utilities.py +0 -6
- edsl/config.py +22 -2
- edsl/conversation/car_buying.py +2 -1
- edsl/coop/CoopFunctionsMixin.py +15 -0
- edsl/coop/ExpectedParrotKeyHandler.py +125 -0
- edsl/coop/PriceFetcher.py +1 -1
- edsl/coop/coop.py +125 -47
- edsl/coop/utils.py +14 -14
- edsl/data/Cache.py +45 -27
- edsl/data/CacheEntry.py +12 -15
- edsl/data/CacheHandler.py +31 -12
- edsl/data/RemoteCacheSync.py +154 -46
- edsl/data/__init__.py +4 -3
- edsl/data_transfer_models.py +2 -1
- edsl/enums.py +27 -0
- edsl/exceptions/__init__.py +50 -50
- edsl/exceptions/agents.py +12 -0
- edsl/exceptions/inference_services.py +5 -0
- edsl/exceptions/questions.py +24 -6
- edsl/exceptions/scenarios.py +7 -0
- edsl/inference_services/AnthropicService.py +38 -19
- edsl/inference_services/AvailableModelCacheHandler.py +184 -0
- edsl/inference_services/AvailableModelFetcher.py +215 -0
- edsl/inference_services/AwsBedrock.py +0 -2
- edsl/inference_services/AzureAI.py +0 -2
- edsl/inference_services/GoogleService.py +7 -12
- edsl/inference_services/InferenceServiceABC.py +18 -85
- edsl/inference_services/InferenceServicesCollection.py +120 -79
- edsl/inference_services/MistralAIService.py +0 -3
- edsl/inference_services/OpenAIService.py +47 -35
- edsl/inference_services/PerplexityService.py +0 -3
- edsl/inference_services/ServiceAvailability.py +135 -0
- edsl/inference_services/TestService.py +11 -10
- edsl/inference_services/TogetherAIService.py +5 -3
- edsl/inference_services/data_structures.py +134 -0
- edsl/jobs/AnswerQuestionFunctionConstructor.py +223 -0
- edsl/jobs/Answers.py +1 -14
- edsl/jobs/FetchInvigilator.py +47 -0
- edsl/jobs/InterviewTaskManager.py +98 -0
- edsl/jobs/InterviewsConstructor.py +50 -0
- edsl/jobs/Jobs.py +356 -431
- edsl/jobs/JobsChecks.py +35 -10
- edsl/jobs/JobsComponentConstructor.py +189 -0
- edsl/jobs/JobsPrompts.py +6 -4
- edsl/jobs/JobsRemoteInferenceHandler.py +205 -133
- edsl/jobs/JobsRemoteInferenceLogger.py +239 -0
- edsl/jobs/RequestTokenEstimator.py +30 -0
- edsl/jobs/async_interview_runner.py +138 -0
- edsl/jobs/buckets/BucketCollection.py +44 -3
- edsl/jobs/buckets/TokenBucket.py +53 -21
- edsl/jobs/buckets/TokenBucketAPI.py +211 -0
- edsl/jobs/buckets/TokenBucketClient.py +191 -0
- edsl/jobs/check_survey_scenario_compatibility.py +85 -0
- edsl/jobs/data_structures.py +120 -0
- edsl/jobs/decorators.py +35 -0
- edsl/jobs/interviews/Interview.py +143 -408
- edsl/jobs/jobs_status_enums.py +9 -0
- edsl/jobs/loggers/HTMLTableJobLogger.py +304 -0
- edsl/jobs/results_exceptions_handler.py +98 -0
- edsl/jobs/runners/JobsRunnerAsyncio.py +88 -403
- edsl/jobs/runners/JobsRunnerStatus.py +133 -165
- edsl/jobs/tasks/QuestionTaskCreator.py +21 -19
- edsl/jobs/tasks/TaskHistory.py +38 -18
- edsl/jobs/tasks/task_status_enum.py +0 -2
- edsl/language_models/ComputeCost.py +63 -0
- edsl/language_models/LanguageModel.py +194 -236
- edsl/language_models/ModelList.py +28 -19
- edsl/language_models/PriceManager.py +127 -0
- edsl/language_models/RawResponseHandler.py +106 -0
- edsl/language_models/ServiceDataSources.py +0 -0
- edsl/language_models/__init__.py +1 -2
- edsl/language_models/key_management/KeyLookup.py +63 -0
- edsl/language_models/key_management/KeyLookupBuilder.py +273 -0
- edsl/language_models/key_management/KeyLookupCollection.py +38 -0
- edsl/language_models/key_management/__init__.py +0 -0
- edsl/language_models/key_management/models.py +131 -0
- edsl/language_models/model.py +256 -0
- edsl/language_models/repair.py +2 -2
- edsl/language_models/utilities.py +5 -4
- edsl/notebooks/Notebook.py +19 -14
- edsl/notebooks/NotebookToLaTeX.py +142 -0
- edsl/prompts/Prompt.py +29 -39
- edsl/questions/ExceptionExplainer.py +77 -0
- edsl/questions/HTMLQuestion.py +103 -0
- edsl/questions/QuestionBase.py +68 -214
- edsl/questions/QuestionBasePromptsMixin.py +7 -3
- edsl/questions/QuestionBudget.py +1 -1
- edsl/questions/QuestionCheckBox.py +3 -3
- edsl/questions/QuestionExtract.py +5 -7
- edsl/questions/QuestionFreeText.py +2 -3
- edsl/questions/QuestionList.py +10 -18
- edsl/questions/QuestionMatrix.py +265 -0
- edsl/questions/QuestionMultipleChoice.py +67 -23
- edsl/questions/QuestionNumerical.py +2 -4
- edsl/questions/QuestionRank.py +7 -17
- edsl/questions/SimpleAskMixin.py +4 -3
- edsl/questions/__init__.py +2 -1
- edsl/questions/{AnswerValidatorMixin.py → answer_validator_mixin.py} +47 -2
- edsl/questions/data_structures.py +20 -0
- edsl/questions/derived/QuestionLinearScale.py +6 -3
- edsl/questions/derived/QuestionTopK.py +1 -1
- edsl/questions/descriptors.py +17 -3
- edsl/questions/loop_processor.py +149 -0
- edsl/questions/{QuestionBaseGenMixin.py → question_base_gen_mixin.py} +57 -50
- edsl/questions/question_registry.py +1 -1
- edsl/questions/{ResponseValidatorABC.py → response_validator_abc.py} +40 -26
- edsl/questions/response_validator_factory.py +34 -0
- edsl/questions/templates/matrix/__init__.py +1 -0
- edsl/questions/templates/matrix/answering_instructions.jinja +5 -0
- edsl/questions/templates/matrix/question_presentation.jinja +20 -0
- edsl/results/CSSParameterizer.py +1 -1
- edsl/results/Dataset.py +170 -7
- edsl/results/DatasetExportMixin.py +168 -305
- edsl/results/DatasetTree.py +28 -8
- edsl/results/MarkdownToDocx.py +122 -0
- edsl/results/MarkdownToPDF.py +111 -0
- edsl/results/Result.py +298 -206
- edsl/results/Results.py +149 -131
- edsl/results/ResultsExportMixin.py +2 -0
- edsl/results/TableDisplay.py +98 -171
- edsl/results/TextEditor.py +50 -0
- edsl/results/__init__.py +1 -1
- edsl/results/file_exports.py +252 -0
- edsl/results/{Selector.py → results_selector.py} +23 -13
- edsl/results/smart_objects.py +96 -0
- edsl/results/table_data_class.py +12 -0
- edsl/results/table_renderers.py +118 -0
- edsl/scenarios/ConstructDownloadLink.py +109 -0
- edsl/scenarios/DocumentChunker.py +102 -0
- edsl/scenarios/DocxScenario.py +16 -0
- edsl/scenarios/FileStore.py +150 -239
- edsl/scenarios/PdfExtractor.py +40 -0
- edsl/scenarios/Scenario.py +90 -193
- edsl/scenarios/ScenarioHtmlMixin.py +4 -3
- edsl/scenarios/ScenarioList.py +415 -244
- edsl/scenarios/ScenarioListExportMixin.py +0 -7
- edsl/scenarios/ScenarioListPdfMixin.py +15 -37
- edsl/scenarios/__init__.py +1 -2
- edsl/scenarios/directory_scanner.py +96 -0
- edsl/scenarios/file_methods.py +85 -0
- edsl/scenarios/handlers/__init__.py +13 -0
- edsl/scenarios/handlers/csv.py +49 -0
- edsl/scenarios/handlers/docx.py +76 -0
- edsl/scenarios/handlers/html.py +37 -0
- edsl/scenarios/handlers/json.py +111 -0
- edsl/scenarios/handlers/latex.py +5 -0
- edsl/scenarios/handlers/md.py +51 -0
- edsl/scenarios/handlers/pdf.py +68 -0
- edsl/scenarios/handlers/png.py +39 -0
- edsl/scenarios/handlers/pptx.py +105 -0
- edsl/scenarios/handlers/py.py +294 -0
- edsl/scenarios/handlers/sql.py +313 -0
- edsl/scenarios/handlers/sqlite.py +149 -0
- edsl/scenarios/handlers/txt.py +33 -0
- edsl/scenarios/{ScenarioJoin.py → scenario_join.py} +10 -6
- edsl/scenarios/scenario_selector.py +156 -0
- edsl/study/ObjectEntry.py +1 -1
- edsl/study/SnapShot.py +1 -1
- edsl/study/Study.py +5 -12
- edsl/surveys/ConstructDAG.py +92 -0
- edsl/surveys/EditSurvey.py +221 -0
- edsl/surveys/InstructionHandler.py +100 -0
- edsl/surveys/MemoryManagement.py +72 -0
- edsl/surveys/Rule.py +5 -4
- edsl/surveys/RuleCollection.py +25 -27
- edsl/surveys/RuleManager.py +172 -0
- edsl/surveys/Simulator.py +75 -0
- edsl/surveys/Survey.py +270 -791
- edsl/surveys/SurveyCSS.py +20 -8
- edsl/surveys/{SurveyFlowVisualizationMixin.py → SurveyFlowVisualization.py} +11 -9
- edsl/surveys/SurveyToApp.py +141 -0
- edsl/surveys/__init__.py +4 -2
- edsl/surveys/descriptors.py +6 -2
- edsl/surveys/instructions/ChangeInstruction.py +1 -2
- edsl/surveys/instructions/Instruction.py +4 -13
- edsl/surveys/instructions/InstructionCollection.py +11 -6
- edsl/templates/error_reporting/interview_details.html +1 -1
- edsl/templates/error_reporting/report.html +1 -1
- edsl/tools/plotting.py +1 -1
- edsl/utilities/PrettyList.py +56 -0
- edsl/utilities/is_notebook.py +18 -0
- edsl/utilities/is_valid_variable_name.py +11 -0
- edsl/utilities/remove_edsl_version.py +24 -0
- edsl/utilities/utilities.py +35 -23
- {edsl-0.1.38.dev4.dist-info → edsl-0.1.39.dist-info}/METADATA +12 -10
- edsl-0.1.39.dist-info/RECORD +358 -0
- {edsl-0.1.38.dev4.dist-info → edsl-0.1.39.dist-info}/WHEEL +1 -1
- edsl/language_models/KeyLookup.py +0 -30
- edsl/language_models/registry.py +0 -190
- edsl/language_models/unused/ReplicateBase.py +0 -83
- edsl/results/ResultsDBMixin.py +0 -238
- edsl-0.1.38.dev4.dist-info/RECORD +0 -277
- /edsl/questions/{RegisterQuestionsMeta.py → register_questions_meta.py} +0 -0
- /edsl/results/{ResultsFetchMixin.py → results_fetch_mixin.py} +0 -0
- /edsl/results/{ResultsToolsMixin.py → results_tools_mixin.py} +0 -0
- {edsl-0.1.38.dev4.dist-info → edsl-0.1.39.dist-info}/LICENSE +0 -0
edsl/surveys/RuleCollection.py
CHANGED
@@ -1,20 +1,17 @@
|
|
1
1
|
"""A collection of rules for a survey."""
|
2
2
|
|
3
3
|
from typing import List, Union, Any, Optional
|
4
|
-
from collections import defaultdict, UserList
|
4
|
+
from collections import defaultdict, UserList, namedtuple
|
5
5
|
|
6
|
-
from edsl.exceptions import (
|
6
|
+
from edsl.exceptions.surveys import (
|
7
7
|
SurveyRuleCannotEvaluateError,
|
8
8
|
SurveyRuleCollectionHasNoRulesAtNodeError,
|
9
9
|
)
|
10
|
-
|
10
|
+
|
11
11
|
from edsl.surveys.Rule import Rule
|
12
12
|
from edsl.surveys.base import EndOfSurvey
|
13
13
|
from edsl.surveys.DAG import DAG
|
14
14
|
|
15
|
-
# from graphlib import TopologicalSorter
|
16
|
-
|
17
|
-
from collections import namedtuple
|
18
15
|
|
19
16
|
NextQuestion = namedtuple(
|
20
17
|
"NextQuestion", "next_q, num_rules_found, expressions_evaluating_to_true, priority"
|
@@ -46,6 +43,24 @@ class RuleCollection(UserList):
|
|
46
43
|
"""
|
47
44
|
return f"RuleCollection(rules={self.data}, num_questions={self.num_questions})"
|
48
45
|
|
46
|
+
def to_dataset(self):
|
47
|
+
"""Return a Dataset object representation of the RuleCollection object."""
|
48
|
+
from edsl.results.Dataset import Dataset
|
49
|
+
|
50
|
+
keys = ["current_q", "expression", "next_q", "priority", "before_rule"]
|
51
|
+
rule_list = {}
|
52
|
+
for rule in sorted(self, key=lambda r: r.current_q):
|
53
|
+
for k in keys:
|
54
|
+
rule_list.setdefault(k, []).append(getattr(rule, k))
|
55
|
+
|
56
|
+
return Dataset([{k: v} for k, v in rule_list.items()])
|
57
|
+
|
58
|
+
def _repr_html_(self):
|
59
|
+
"""Return an HTML representation of the RuleCollection object."""
|
60
|
+
from edsl.results.Dataset import Dataset
|
61
|
+
|
62
|
+
return self.to_dataset()._repr_html_()
|
63
|
+
|
49
64
|
def to_dict(self, add_edsl_version=True):
|
50
65
|
"""Create a dictionary representation of the RuleCollection object."""
|
51
66
|
return {
|
@@ -106,12 +121,7 @@ class RuleCollection(UserList):
|
|
106
121
|
│ 1 │ q1 == 'no' │ 2 │ 1 │ False │
|
107
122
|
└───────────┴─────────────┴────────┴──────────┴─────────────┘
|
108
123
|
"""
|
109
|
-
|
110
|
-
rule_list = []
|
111
|
-
for rule in sorted(self, key=lambda r: r.current_q):
|
112
|
-
rule_list.append({k: getattr(rule, k) for k in keys})
|
113
|
-
|
114
|
-
print_table_with_rich(rule_list)
|
124
|
+
return self.to_dataset()
|
115
125
|
|
116
126
|
def skip_question_before_running(self, q_now: int, answers: dict[str, Any]) -> bool:
|
117
127
|
"""Determine if a question should be skipped before running the question.
|
@@ -183,17 +193,6 @@ class RuleCollection(UserList):
|
|
183
193
|
NextQuestion(next_q=3, num_rules_found=2, expressions_evaluating_to_true=1, priority=1)
|
184
194
|
|
185
195
|
"""
|
186
|
-
# # is this the first question? If it is, we need to check if it should be skipped.
|
187
|
-
# if q_now == 0:
|
188
|
-
# if self.skip_question_before_running(q_now, answers):
|
189
|
-
# return NextQuestion(
|
190
|
-
# next_q=q_now + 1,
|
191
|
-
# num_rules_found=0,
|
192
|
-
# expressions_evaluating_to_true=0,
|
193
|
-
# priority=-1,
|
194
|
-
# )
|
195
|
-
|
196
|
-
# breakpoint()
|
197
196
|
expressions_evaluating_to_true = 0
|
198
197
|
next_q = None
|
199
198
|
highest_priority = -2 # start with -2 to 'pick up' the default rule added
|
@@ -215,7 +214,6 @@ class RuleCollection(UserList):
|
|
215
214
|
f"No rules found for question {q_now}"
|
216
215
|
)
|
217
216
|
|
218
|
-
# breakpoint()
|
219
217
|
## Now we need to check if the *next question* has any 'before; rules that we should follow
|
220
218
|
for rule in self.applicable_rules(next_q, before_rule=True):
|
221
219
|
if rule.evaluate(answers): # rule evaluates to True
|
@@ -260,8 +258,7 @@ class RuleCollection(UserList):
|
|
260
258
|
[2, 3]
|
261
259
|
|
262
260
|
"""
|
263
|
-
# If it's the end of the survey, all questions between the start_q and the end of the survey
|
264
|
-
# now depend on the start_q
|
261
|
+
# If it's the end of the survey, all questions between the start_q and the end of the survey now depend on the start_q
|
265
262
|
if end_q == EndOfSurvey:
|
266
263
|
if self.num_questions is None:
|
267
264
|
raise ValueError(
|
@@ -381,7 +378,8 @@ class RuleCollection(UserList):
|
|
381
378
|
|
382
379
|
|
383
380
|
if __name__ == "__main__":
|
384
|
-
# pass
|
385
381
|
import doctest
|
386
382
|
|
387
383
|
doctest.testmod(optionflags=doctest.ELLIPSIS)
|
384
|
+
|
385
|
+
print(RuleCollection.example()._repr_html_())
|
@@ -0,0 +1,172 @@
|
|
1
|
+
from typing import Union, TYPE_CHECKING
|
2
|
+
|
3
|
+
if TYPE_CHECKING:
|
4
|
+
from edsl.questions.QuestionBase import QuestionBase
|
5
|
+
|
6
|
+
from edsl.surveys.Rule import Rule
|
7
|
+
from .base import RulePriority, EndOfSurvey
|
8
|
+
from edsl.exceptions.surveys import SurveyError, SurveyCreationError
|
9
|
+
|
10
|
+
|
11
|
+
class ValidatedString(str):
|
12
|
+
def __new__(cls, content):
|
13
|
+
if "<>" in content:
|
14
|
+
raise SurveyCreationError(
|
15
|
+
"The expression contains '<>', which is not allowed. You probably mean '!='."
|
16
|
+
)
|
17
|
+
return super().__new__(cls, content)
|
18
|
+
|
19
|
+
|
20
|
+
class RuleManager:
|
21
|
+
def __init__(self, survey):
|
22
|
+
self.survey = survey
|
23
|
+
|
24
|
+
def _get_question_index(
|
25
|
+
self, q: Union["QuestionBase", str, EndOfSurvey.__class__]
|
26
|
+
) -> Union[int, EndOfSurvey.__class__]:
|
27
|
+
"""Return the index of the question or EndOfSurvey object.
|
28
|
+
|
29
|
+
:param q: The question or question name to get the index of.
|
30
|
+
|
31
|
+
It can handle it if the user passes in the question name, the question object, or the EndOfSurvey object.
|
32
|
+
|
33
|
+
>>> from edsl.questions import QuestionFreeText
|
34
|
+
>>> from edsl import Survey
|
35
|
+
>>> s = Survey.example()
|
36
|
+
>>> s._get_question_index("q0")
|
37
|
+
0
|
38
|
+
|
39
|
+
This doesnt' work with questions that don't exist:
|
40
|
+
|
41
|
+
>>> s._get_question_index("poop")
|
42
|
+
Traceback (most recent call last):
|
43
|
+
...
|
44
|
+
edsl.exceptions.surveys.SurveyError: Question name poop not found in survey. The current question names are {'q0': 0, 'q1': 1, 'q2': 2}.
|
45
|
+
...
|
46
|
+
"""
|
47
|
+
if q == EndOfSurvey:
|
48
|
+
return EndOfSurvey
|
49
|
+
else:
|
50
|
+
question_name = q if isinstance(q, str) else q.question_name
|
51
|
+
if question_name not in self.survey.question_name_to_index:
|
52
|
+
raise SurveyError(
|
53
|
+
f"""Question name {question_name} not found in survey. The current question names are {self.survey.question_name_to_index}."""
|
54
|
+
)
|
55
|
+
return self.survey.question_name_to_index[question_name]
|
56
|
+
|
57
|
+
def _get_new_rule_priority(
|
58
|
+
self, question_index: int, before_rule: bool = False
|
59
|
+
) -> int:
|
60
|
+
"""Return the priority for the new rule.
|
61
|
+
|
62
|
+
:param question_index: The index of the question to add the rule to.
|
63
|
+
:param before_rule: Whether the rule is evaluated before the question is answered.
|
64
|
+
|
65
|
+
>>> from edsl import Survey
|
66
|
+
>>> s = Survey.example()
|
67
|
+
>>> RuleManager(s)._get_new_rule_priority(0)
|
68
|
+
1
|
69
|
+
"""
|
70
|
+
current_priorities = [
|
71
|
+
rule.priority
|
72
|
+
for rule in self.survey.rule_collection.applicable_rules(
|
73
|
+
question_index, before_rule
|
74
|
+
)
|
75
|
+
]
|
76
|
+
if len(current_priorities) == 0:
|
77
|
+
return RulePriority.DEFAULT.value + 1
|
78
|
+
|
79
|
+
max_priority = max(current_priorities)
|
80
|
+
# newer rules take priority over older rules
|
81
|
+
new_priority = (
|
82
|
+
RulePriority.DEFAULT.value
|
83
|
+
if len(current_priorities) == 0
|
84
|
+
else max_priority + 1
|
85
|
+
)
|
86
|
+
return new_priority
|
87
|
+
|
88
|
+
def add_rule(
|
89
|
+
self,
|
90
|
+
question: Union["QuestionBase", str],
|
91
|
+
expression: str,
|
92
|
+
next_question: Union["QuestionBase", str, int],
|
93
|
+
before_rule: bool = False,
|
94
|
+
) -> "Survey":
|
95
|
+
"""
|
96
|
+
Add a rule to a Question of the Survey with the appropriate priority.
|
97
|
+
|
98
|
+
:param question: The question to add the rule to.
|
99
|
+
:param expression: The expression to evaluate.
|
100
|
+
:param next_question: The next question to go to if the rule is true.
|
101
|
+
:param before_rule: Whether the rule is evaluated before the question is answered.
|
102
|
+
|
103
|
+
|
104
|
+
- The last rule added for the question will have the highest priority.
|
105
|
+
- If there are no rules, the rule added gets priority -1.
|
106
|
+
"""
|
107
|
+
question_index = self.survey._get_question_index(question) # Fix
|
108
|
+
|
109
|
+
# Might not have the name of the next question yet
|
110
|
+
if isinstance(next_question, int):
|
111
|
+
next_question_index = next_question
|
112
|
+
else:
|
113
|
+
next_question_index = self._get_question_index(next_question)
|
114
|
+
|
115
|
+
new_priority = self._get_new_rule_priority(question_index, before_rule) # fix
|
116
|
+
|
117
|
+
self.survey.rule_collection.add_rule(
|
118
|
+
Rule(
|
119
|
+
current_q=question_index,
|
120
|
+
expression=expression,
|
121
|
+
next_q=next_question_index,
|
122
|
+
question_name_to_index=self.survey.question_name_to_index,
|
123
|
+
priority=new_priority,
|
124
|
+
before_rule=before_rule,
|
125
|
+
)
|
126
|
+
)
|
127
|
+
|
128
|
+
return self.survey
|
129
|
+
|
130
|
+
def add_stop_rule(
|
131
|
+
self, question: Union["QuestionBase", str], expression: str
|
132
|
+
) -> "Survey":
|
133
|
+
"""Add a rule that stops the survey.
|
134
|
+
The rule is evaluated *after* the question is answered. If the rule is true, the survey ends.
|
135
|
+
|
136
|
+
:param question: The question to add the stop rule to.
|
137
|
+
:param expression: The expression to evaluate.
|
138
|
+
|
139
|
+
If this rule is true, the survey ends.
|
140
|
+
|
141
|
+
Here, answering "yes" to q0 ends the survey:
|
142
|
+
|
143
|
+
>>> from edsl import Survey
|
144
|
+
>>> s = Survey.example().add_stop_rule("q0", "q0 == 'yes'")
|
145
|
+
>>> s.next_question("q0", {"q0": "yes"})
|
146
|
+
EndOfSurvey
|
147
|
+
|
148
|
+
By comparison, answering "no" to q0 does not end the survey:
|
149
|
+
|
150
|
+
>>> s.next_question("q0", {"q0": "no"}).question_name
|
151
|
+
'q1'
|
152
|
+
|
153
|
+
>>> s.add_stop_rule("q0", "q1 <> 'yes'")
|
154
|
+
Traceback (most recent call last):
|
155
|
+
...
|
156
|
+
edsl.exceptions.surveys.SurveyCreationError: The expression contains '<>', which is not allowed. You probably mean '!='.
|
157
|
+
...
|
158
|
+
"""
|
159
|
+
expression = ValidatedString(expression)
|
160
|
+
prior_question_appears = False
|
161
|
+
for prior_question in self.survey.questions:
|
162
|
+
if prior_question.question_name in expression:
|
163
|
+
prior_question_appears = True
|
164
|
+
|
165
|
+
if not prior_question_appears:
|
166
|
+
import warnings
|
167
|
+
|
168
|
+
warnings.warn(
|
169
|
+
f"The expression {expression} does not contain any prior question names. This is probably a mistake."
|
170
|
+
)
|
171
|
+
self.survey.add_rule(question, expression, EndOfSurvey)
|
172
|
+
return self.survey
|
@@ -0,0 +1,75 @@
|
|
1
|
+
from typing import Callable
|
2
|
+
|
3
|
+
|
4
|
+
class Simulator:
|
5
|
+
def __init__(self, survey):
|
6
|
+
self.survey = survey
|
7
|
+
|
8
|
+
@classmethod
|
9
|
+
def random_survey(cls):
|
10
|
+
"""Create a random survey."""
|
11
|
+
from edsl.questions import QuestionMultipleChoice, QuestionFreeText
|
12
|
+
from random import choice
|
13
|
+
from edsl.surveys.Survey import Survey
|
14
|
+
|
15
|
+
num_questions = 10
|
16
|
+
questions = []
|
17
|
+
for i in range(num_questions):
|
18
|
+
if choice([True, False]):
|
19
|
+
q = QuestionMultipleChoice(
|
20
|
+
question_text="nothing",
|
21
|
+
question_name="q_" + str(i),
|
22
|
+
question_options=list(range(3)),
|
23
|
+
)
|
24
|
+
questions.append(q)
|
25
|
+
else:
|
26
|
+
questions.append(
|
27
|
+
QuestionFreeText(
|
28
|
+
question_text="nothing", question_name="q_" + str(i)
|
29
|
+
)
|
30
|
+
)
|
31
|
+
s = Survey(questions)
|
32
|
+
start_index = choice(range(num_questions - 1))
|
33
|
+
end_index = choice(range(start_index + 1, 10))
|
34
|
+
s = s.add_rule(f"q_{start_index}", "True", f"q_{end_index}")
|
35
|
+
question_to_delete = choice(range(num_questions))
|
36
|
+
s.delete_question(f"q_{question_to_delete}")
|
37
|
+
return s
|
38
|
+
|
39
|
+
def simulate(self) -> dict:
|
40
|
+
"""Simulate the survey and return the answers."""
|
41
|
+
i = self.survey.gen_path_through_survey()
|
42
|
+
q = next(i)
|
43
|
+
num_passes = 0
|
44
|
+
while True:
|
45
|
+
num_passes += 1
|
46
|
+
try:
|
47
|
+
answer = q._simulate_answer()
|
48
|
+
q = i.send({q.question_name: answer["answer"]})
|
49
|
+
except StopIteration:
|
50
|
+
break
|
51
|
+
|
52
|
+
if num_passes > 100:
|
53
|
+
print("Too many passes.")
|
54
|
+
raise Exception("Too many passes.")
|
55
|
+
return self.survey.answers
|
56
|
+
|
57
|
+
def create_agent(self) -> "Agent":
|
58
|
+
"""Create an agent from the simulated answers."""
|
59
|
+
answers_dict = self.survey.simulate()
|
60
|
+
from edsl.agents.Agent import Agent
|
61
|
+
|
62
|
+
def construct_answer_dict_function(traits: dict) -> Callable:
|
63
|
+
def func(self, question: "QuestionBase", scenario=None):
|
64
|
+
return traits.get(question.question_name, None)
|
65
|
+
|
66
|
+
return func
|
67
|
+
|
68
|
+
return Agent(traits=answers_dict).add_direct_question_answering_method(
|
69
|
+
construct_answer_dict_function(answers_dict)
|
70
|
+
)
|
71
|
+
|
72
|
+
def simulate_results(self) -> "Results":
|
73
|
+
"""Simulate the survey and return the results."""
|
74
|
+
a = self.create_agent()
|
75
|
+
return self.survey.by([a]).run()
|