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