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.
Files changed (212) hide show
  1. edsl/Base.py +197 -116
  2. edsl/__init__.py +15 -7
  3. edsl/__version__.py +1 -1
  4. edsl/agents/Agent.py +351 -147
  5. edsl/agents/AgentList.py +211 -73
  6. edsl/agents/Invigilator.py +101 -50
  7. edsl/agents/InvigilatorBase.py +62 -70
  8. edsl/agents/PromptConstructor.py +143 -225
  9. edsl/agents/QuestionInstructionPromptBuilder.py +128 -0
  10. edsl/agents/QuestionTemplateReplacementsBuilder.py +137 -0
  11. edsl/agents/__init__.py +0 -1
  12. edsl/agents/prompt_helpers.py +3 -3
  13. edsl/agents/question_option_processor.py +172 -0
  14. edsl/auto/AutoStudy.py +18 -5
  15. edsl/auto/StageBase.py +53 -40
  16. edsl/auto/StageQuestions.py +2 -1
  17. edsl/auto/utilities.py +0 -6
  18. edsl/config.py +22 -2
  19. edsl/conversation/car_buying.py +2 -1
  20. edsl/coop/CoopFunctionsMixin.py +15 -0
  21. edsl/coop/ExpectedParrotKeyHandler.py +125 -0
  22. edsl/coop/PriceFetcher.py +1 -1
  23. edsl/coop/coop.py +125 -47
  24. edsl/coop/utils.py +14 -14
  25. edsl/data/Cache.py +45 -27
  26. edsl/data/CacheEntry.py +12 -15
  27. edsl/data/CacheHandler.py +31 -12
  28. edsl/data/RemoteCacheSync.py +154 -46
  29. edsl/data/__init__.py +4 -3
  30. edsl/data_transfer_models.py +2 -1
  31. edsl/enums.py +27 -0
  32. edsl/exceptions/__init__.py +50 -50
  33. edsl/exceptions/agents.py +12 -0
  34. edsl/exceptions/inference_services.py +5 -0
  35. edsl/exceptions/questions.py +24 -6
  36. edsl/exceptions/scenarios.py +7 -0
  37. edsl/inference_services/AnthropicService.py +38 -19
  38. edsl/inference_services/AvailableModelCacheHandler.py +184 -0
  39. edsl/inference_services/AvailableModelFetcher.py +215 -0
  40. edsl/inference_services/AwsBedrock.py +0 -2
  41. edsl/inference_services/AzureAI.py +0 -2
  42. edsl/inference_services/GoogleService.py +7 -12
  43. edsl/inference_services/InferenceServiceABC.py +18 -85
  44. edsl/inference_services/InferenceServicesCollection.py +120 -79
  45. edsl/inference_services/MistralAIService.py +0 -3
  46. edsl/inference_services/OpenAIService.py +47 -35
  47. edsl/inference_services/PerplexityService.py +0 -3
  48. edsl/inference_services/ServiceAvailability.py +135 -0
  49. edsl/inference_services/TestService.py +11 -10
  50. edsl/inference_services/TogetherAIService.py +5 -3
  51. edsl/inference_services/data_structures.py +134 -0
  52. edsl/jobs/AnswerQuestionFunctionConstructor.py +223 -0
  53. edsl/jobs/Answers.py +1 -14
  54. edsl/jobs/FetchInvigilator.py +47 -0
  55. edsl/jobs/InterviewTaskManager.py +98 -0
  56. edsl/jobs/InterviewsConstructor.py +50 -0
  57. edsl/jobs/Jobs.py +356 -431
  58. edsl/jobs/JobsChecks.py +35 -10
  59. edsl/jobs/JobsComponentConstructor.py +189 -0
  60. edsl/jobs/JobsPrompts.py +6 -4
  61. edsl/jobs/JobsRemoteInferenceHandler.py +205 -133
  62. edsl/jobs/JobsRemoteInferenceLogger.py +239 -0
  63. edsl/jobs/RequestTokenEstimator.py +30 -0
  64. edsl/jobs/async_interview_runner.py +138 -0
  65. edsl/jobs/buckets/BucketCollection.py +44 -3
  66. edsl/jobs/buckets/TokenBucket.py +53 -21
  67. edsl/jobs/buckets/TokenBucketAPI.py +211 -0
  68. edsl/jobs/buckets/TokenBucketClient.py +191 -0
  69. edsl/jobs/check_survey_scenario_compatibility.py +85 -0
  70. edsl/jobs/data_structures.py +120 -0
  71. edsl/jobs/decorators.py +35 -0
  72. edsl/jobs/interviews/Interview.py +143 -408
  73. edsl/jobs/jobs_status_enums.py +9 -0
  74. edsl/jobs/loggers/HTMLTableJobLogger.py +304 -0
  75. edsl/jobs/results_exceptions_handler.py +98 -0
  76. edsl/jobs/runners/JobsRunnerAsyncio.py +88 -403
  77. edsl/jobs/runners/JobsRunnerStatus.py +133 -165
  78. edsl/jobs/tasks/QuestionTaskCreator.py +21 -19
  79. edsl/jobs/tasks/TaskHistory.py +38 -18
  80. edsl/jobs/tasks/task_status_enum.py +0 -2
  81. edsl/language_models/ComputeCost.py +63 -0
  82. edsl/language_models/LanguageModel.py +194 -236
  83. edsl/language_models/ModelList.py +28 -19
  84. edsl/language_models/PriceManager.py +127 -0
  85. edsl/language_models/RawResponseHandler.py +106 -0
  86. edsl/language_models/ServiceDataSources.py +0 -0
  87. edsl/language_models/__init__.py +1 -2
  88. edsl/language_models/key_management/KeyLookup.py +63 -0
  89. edsl/language_models/key_management/KeyLookupBuilder.py +273 -0
  90. edsl/language_models/key_management/KeyLookupCollection.py +38 -0
  91. edsl/language_models/key_management/__init__.py +0 -0
  92. edsl/language_models/key_management/models.py +131 -0
  93. edsl/language_models/model.py +256 -0
  94. edsl/language_models/repair.py +2 -2
  95. edsl/language_models/utilities.py +5 -4
  96. edsl/notebooks/Notebook.py +19 -14
  97. edsl/notebooks/NotebookToLaTeX.py +142 -0
  98. edsl/prompts/Prompt.py +29 -39
  99. edsl/questions/ExceptionExplainer.py +77 -0
  100. edsl/questions/HTMLQuestion.py +103 -0
  101. edsl/questions/QuestionBase.py +68 -214
  102. edsl/questions/QuestionBasePromptsMixin.py +7 -3
  103. edsl/questions/QuestionBudget.py +1 -1
  104. edsl/questions/QuestionCheckBox.py +3 -3
  105. edsl/questions/QuestionExtract.py +5 -7
  106. edsl/questions/QuestionFreeText.py +2 -3
  107. edsl/questions/QuestionList.py +10 -18
  108. edsl/questions/QuestionMatrix.py +265 -0
  109. edsl/questions/QuestionMultipleChoice.py +67 -23
  110. edsl/questions/QuestionNumerical.py +2 -4
  111. edsl/questions/QuestionRank.py +7 -17
  112. edsl/questions/SimpleAskMixin.py +4 -3
  113. edsl/questions/__init__.py +2 -1
  114. edsl/questions/{AnswerValidatorMixin.py → answer_validator_mixin.py} +47 -2
  115. edsl/questions/data_structures.py +20 -0
  116. edsl/questions/derived/QuestionLinearScale.py +6 -3
  117. edsl/questions/derived/QuestionTopK.py +1 -1
  118. edsl/questions/descriptors.py +17 -3
  119. edsl/questions/loop_processor.py +149 -0
  120. edsl/questions/{QuestionBaseGenMixin.py → question_base_gen_mixin.py} +57 -50
  121. edsl/questions/question_registry.py +1 -1
  122. edsl/questions/{ResponseValidatorABC.py → response_validator_abc.py} +40 -26
  123. edsl/questions/response_validator_factory.py +34 -0
  124. edsl/questions/templates/matrix/__init__.py +1 -0
  125. edsl/questions/templates/matrix/answering_instructions.jinja +5 -0
  126. edsl/questions/templates/matrix/question_presentation.jinja +20 -0
  127. edsl/results/CSSParameterizer.py +1 -1
  128. edsl/results/Dataset.py +170 -7
  129. edsl/results/DatasetExportMixin.py +168 -305
  130. edsl/results/DatasetTree.py +28 -8
  131. edsl/results/MarkdownToDocx.py +122 -0
  132. edsl/results/MarkdownToPDF.py +111 -0
  133. edsl/results/Result.py +298 -206
  134. edsl/results/Results.py +149 -131
  135. edsl/results/ResultsExportMixin.py +2 -0
  136. edsl/results/TableDisplay.py +98 -171
  137. edsl/results/TextEditor.py +50 -0
  138. edsl/results/__init__.py +1 -1
  139. edsl/results/file_exports.py +252 -0
  140. edsl/results/{Selector.py → results_selector.py} +23 -13
  141. edsl/results/smart_objects.py +96 -0
  142. edsl/results/table_data_class.py +12 -0
  143. edsl/results/table_renderers.py +118 -0
  144. edsl/scenarios/ConstructDownloadLink.py +109 -0
  145. edsl/scenarios/DocumentChunker.py +102 -0
  146. edsl/scenarios/DocxScenario.py +16 -0
  147. edsl/scenarios/FileStore.py +150 -239
  148. edsl/scenarios/PdfExtractor.py +40 -0
  149. edsl/scenarios/Scenario.py +90 -193
  150. edsl/scenarios/ScenarioHtmlMixin.py +4 -3
  151. edsl/scenarios/ScenarioList.py +415 -244
  152. edsl/scenarios/ScenarioListExportMixin.py +0 -7
  153. edsl/scenarios/ScenarioListPdfMixin.py +15 -37
  154. edsl/scenarios/__init__.py +1 -2
  155. edsl/scenarios/directory_scanner.py +96 -0
  156. edsl/scenarios/file_methods.py +85 -0
  157. edsl/scenarios/handlers/__init__.py +13 -0
  158. edsl/scenarios/handlers/csv.py +49 -0
  159. edsl/scenarios/handlers/docx.py +76 -0
  160. edsl/scenarios/handlers/html.py +37 -0
  161. edsl/scenarios/handlers/json.py +111 -0
  162. edsl/scenarios/handlers/latex.py +5 -0
  163. edsl/scenarios/handlers/md.py +51 -0
  164. edsl/scenarios/handlers/pdf.py +68 -0
  165. edsl/scenarios/handlers/png.py +39 -0
  166. edsl/scenarios/handlers/pptx.py +105 -0
  167. edsl/scenarios/handlers/py.py +294 -0
  168. edsl/scenarios/handlers/sql.py +313 -0
  169. edsl/scenarios/handlers/sqlite.py +149 -0
  170. edsl/scenarios/handlers/txt.py +33 -0
  171. edsl/scenarios/{ScenarioJoin.py → scenario_join.py} +10 -6
  172. edsl/scenarios/scenario_selector.py +156 -0
  173. edsl/study/ObjectEntry.py +1 -1
  174. edsl/study/SnapShot.py +1 -1
  175. edsl/study/Study.py +5 -12
  176. edsl/surveys/ConstructDAG.py +92 -0
  177. edsl/surveys/EditSurvey.py +221 -0
  178. edsl/surveys/InstructionHandler.py +100 -0
  179. edsl/surveys/MemoryManagement.py +72 -0
  180. edsl/surveys/Rule.py +5 -4
  181. edsl/surveys/RuleCollection.py +25 -27
  182. edsl/surveys/RuleManager.py +172 -0
  183. edsl/surveys/Simulator.py +75 -0
  184. edsl/surveys/Survey.py +270 -791
  185. edsl/surveys/SurveyCSS.py +20 -8
  186. edsl/surveys/{SurveyFlowVisualizationMixin.py → SurveyFlowVisualization.py} +11 -9
  187. edsl/surveys/SurveyToApp.py +141 -0
  188. edsl/surveys/__init__.py +4 -2
  189. edsl/surveys/descriptors.py +6 -2
  190. edsl/surveys/instructions/ChangeInstruction.py +1 -2
  191. edsl/surveys/instructions/Instruction.py +4 -13
  192. edsl/surveys/instructions/InstructionCollection.py +11 -6
  193. edsl/templates/error_reporting/interview_details.html +1 -1
  194. edsl/templates/error_reporting/report.html +1 -1
  195. edsl/tools/plotting.py +1 -1
  196. edsl/utilities/PrettyList.py +56 -0
  197. edsl/utilities/is_notebook.py +18 -0
  198. edsl/utilities/is_valid_variable_name.py +11 -0
  199. edsl/utilities/remove_edsl_version.py +24 -0
  200. edsl/utilities/utilities.py +35 -23
  201. {edsl-0.1.38.dev4.dist-info → edsl-0.1.39.dist-info}/METADATA +12 -10
  202. edsl-0.1.39.dist-info/RECORD +358 -0
  203. {edsl-0.1.38.dev4.dist-info → edsl-0.1.39.dist-info}/WHEEL +1 -1
  204. edsl/language_models/KeyLookup.py +0 -30
  205. edsl/language_models/registry.py +0 -190
  206. edsl/language_models/unused/ReplicateBase.py +0 -83
  207. edsl/results/ResultsDBMixin.py +0 -238
  208. edsl-0.1.38.dev4.dist-info/RECORD +0 -277
  209. /edsl/questions/{RegisterQuestionsMeta.py → register_questions_meta.py} +0 -0
  210. /edsl/results/{ResultsFetchMixin.py → results_fetch_mixin.py} +0 -0
  211. /edsl/results/{ResultsToolsMixin.py → results_tools_mixin.py} +0 -0
  212. {edsl-0.1.38.dev4.dist-info → edsl-0.1.39.dist-info}/LICENSE +0 -0
edsl/surveys/Survey.py CHANGED
@@ -2,43 +2,93 @@
2
2
 
3
3
  from __future__ import annotations
4
4
  import re
5
- import tempfile
6
- import requests
7
-
8
- from typing import Any, Generator, Optional, Union, List, Literal, Callable
5
+ import random
6
+
7
+ from typing import (
8
+ Any,
9
+ Generator,
10
+ Optional,
11
+ Union,
12
+ List,
13
+ Literal,
14
+ Callable,
15
+ TYPE_CHECKING,
16
+ )
9
17
  from uuid import uuid4
10
18
  from edsl.Base import Base
11
- from edsl.exceptions import SurveyCreationError, SurveyHasNoRulesError
19
+ from edsl.exceptions.surveys import SurveyCreationError, SurveyHasNoRulesError
12
20
  from edsl.exceptions.surveys import SurveyError
21
+ from collections import UserDict
13
22
 
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
23
 
25
- from edsl.agents.Agent import Agent
24
+ class PseudoIndices(UserDict):
25
+ @property
26
+ def max_pseudo_index(self) -> float:
27
+ """Return the maximum pseudo index in the survey.
28
+ >>> Survey.example()._pseudo_indices.max_pseudo_index
29
+ 2
30
+ """
31
+ if len(self) == 0:
32
+ return -1
33
+ return max(self.values())
34
+
35
+ @property
36
+ def last_item_was_instruction(self) -> bool:
37
+ """Return whether the last item added to the survey was an instruction.
38
+
39
+ This is used to determine the pseudo-index of the next item added to the survey.
40
+
41
+ Example:
42
+
43
+ >>> s = Survey.example()
44
+ >>> s._pseudo_indices.last_item_was_instruction
45
+ False
46
+ >>> from edsl.surveys.instructions.Instruction import Instruction
47
+ >>> s = s.add_instruction(Instruction(text="Pay attention to the following questions.", name="intro"))
48
+ >>> s._pseudo_indices.last_item_was_instruction
49
+ True
50
+ """
51
+ return isinstance(self.max_pseudo_index, float)
52
+
53
+
54
+ if TYPE_CHECKING:
55
+ from edsl.questions.QuestionBase import QuestionBase
56
+ from edsl.agents.Agent import Agent
57
+ from edsl.surveys.DAG import DAG
58
+ from edsl.language_models.LanguageModel import LanguageModel
59
+ from edsl.scenarios.Scenario import Scenario
60
+ from edsl.data.Cache import Cache
61
+
62
+ # This is a hack to get around the fact that TypeAlias is not available in typing until Python 3.10
63
+ try:
64
+ from typing import TypeAlias
65
+ except ImportError:
66
+ from typing import _GenericAlias as TypeAlias
67
+
68
+ QuestionType: TypeAlias = Union[QuestionBase, Instruction, ChangeInstruction]
69
+ QuestionGroupType: TypeAlias = dict[str, tuple[int, int]]
70
+
71
+
72
+ from edsl.utilities.remove_edsl_version import remove_edsl_version
26
73
 
27
74
  from edsl.surveys.instructions.InstructionCollection import InstructionCollection
28
75
  from edsl.surveys.instructions.Instruction import Instruction
29
76
  from edsl.surveys.instructions.ChangeInstruction import ChangeInstruction
30
77
 
31
-
32
- class ValidatedString(str):
33
- def __new__(cls, content):
34
- if "<>" in content:
35
- raise SurveyCreationError(
36
- "The expression contains '<>', which is not allowed. You probably mean '!='."
37
- )
38
- return super().__new__(cls, content)
78
+ from edsl.surveys.base import EndOfSurvey
79
+ from edsl.surveys.descriptors import QuestionsDescriptor
80
+ from edsl.surveys.MemoryPlan import MemoryPlan
81
+ from edsl.surveys.RuleCollection import RuleCollection
82
+ from edsl.surveys.SurveyExportMixin import SurveyExportMixin
83
+ from edsl.surveys.SurveyFlowVisualization import SurveyFlowVisualization
84
+ from edsl.surveys.InstructionHandler import InstructionHandler
85
+ from edsl.surveys.EditSurvey import EditSurvey
86
+ from edsl.surveys.Simulator import Simulator
87
+ from edsl.surveys.MemoryManagement import MemoryManagement
88
+ from edsl.surveys.RuleManager import RuleManager
39
89
 
40
90
 
41
- class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
91
+ class Survey(SurveyExportMixin, Base):
42
92
  """A collection of questions that supports skip logic."""
43
93
 
44
94
  __documentation__ = """https://docs.expectedparrot.com/en/latest/surveys.html"""
@@ -61,13 +111,12 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
61
111
 
62
112
  def __init__(
63
113
  self,
64
- questions: Optional[
65
- list[Union[QuestionBase, Instruction, ChangeInstruction]]
66
- ] = None,
67
- memory_plan: Optional[MemoryPlan] = None,
68
- rule_collection: Optional[RuleCollection] = None,
69
- question_groups: Optional[dict[str, tuple[int, int]]] = None,
114
+ questions: Optional[List["QuestionType"]] = None,
115
+ memory_plan: Optional["MemoryPlan"] = None,
116
+ rule_collection: Optional["RuleCollection"] = None,
117
+ question_groups: Optional["QuestionGroupType"] = None,
70
118
  name: Optional[str] = None,
119
+ questions_to_randomize: Optional[List[str]] = None,
71
120
  ):
72
121
  """Create a new survey.
73
122
 
@@ -89,11 +138,7 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
89
138
 
90
139
  self.raw_passed_questions = questions
91
140
 
92
- (
93
- true_questions,
94
- instruction_names_to_instructions,
95
- self.pseudo_indices,
96
- ) = self._separate_questions_and_instructions(questions or [])
141
+ true_questions = self._process_raw_questions(self.raw_passed_questions)
97
142
 
98
143
  self.rule_collection = RuleCollection(
99
144
  num_questions=len(true_questions) if true_questions else None
@@ -101,8 +146,9 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
101
146
  # the RuleCollection needs to be present while we add the questions; we might override this later
102
147
  # if a rule_collection is provided. This allows us to serialize the survey with the rule_collection.
103
148
 
149
+ # this is where the Questions constructor is called.
104
150
  self.questions = true_questions
105
- self.instruction_names_to_instructions = instruction_names_to_instructions
151
+ # self.instruction_names_to_instructions = instruction_names_to_instructions
106
152
 
107
153
  self.memory_plan = memory_plan or MemoryPlan(self)
108
154
  if question_groups is not None:
@@ -110,7 +156,7 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
110
156
  else:
111
157
  self.question_groups = {}
112
158
 
113
- # if a rule collection is provided, use it instead
159
+ # if a rule collection is provided, use it instead of the constructed one
114
160
  if rule_collection is not None:
115
161
  self.rule_collection = rule_collection
116
162
 
@@ -119,97 +165,58 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
119
165
 
120
166
  warnings.warn("name parameter to a survey is deprecated.")
121
167
 
122
- # region: Suvry instruction handling
168
+ if questions_to_randomize is not None:
169
+ self.questions_to_randomize = questions_to_randomize
170
+ else:
171
+ self.questions_to_randomize = []
172
+
173
+ self._seed = None
174
+
175
+ def draw(self) -> "Survey":
176
+ """Return a new survey with a randomly selected permutation of the options."""
177
+ if self._seed is None: # only set once
178
+ self._seed = hash(self)
179
+ random.seed(self._seed)
180
+
181
+ if len(self.questions_to_randomize) == 0:
182
+ return self
183
+
184
+ new_questions = []
185
+ for question in self.questions:
186
+ if question.question_name in self.questions_to_randomize:
187
+ new_questions.append(question.draw())
188
+ else:
189
+ new_questions.append(question.duplicate())
190
+
191
+ d = self.to_dict()
192
+ d["questions"] = [q.to_dict() for q in new_questions]
193
+ return Survey.from_dict(d)
194
+
195
+ def _process_raw_questions(self, questions: Optional[List["QuestionType"]]) -> list:
196
+ """Process the raw questions passed to the survey."""
197
+ handler = InstructionHandler(self)
198
+ components = handler.separate_questions_and_instructions(questions or [])
199
+ self._instruction_names_to_instructions = (
200
+ components.instruction_names_to_instructions
201
+ )
202
+ self._pseudo_indices = PseudoIndices(components.pseudo_indices)
203
+ return components.true_questions
204
+
205
+ # region: Survey instruction handling
123
206
  @property
124
- def relevant_instructions_dict(self) -> InstructionCollection:
207
+ def _relevant_instructions_dict(self) -> InstructionCollection:
125
208
  """Return a dictionary with keys as question names and values as instructions that are relevant to the question.
126
209
 
127
210
  >>> s = Survey.example(include_instructions=True)
128
- >>> s.relevant_instructions_dict
211
+ >>> s._relevant_instructions_dict
129
212
  {'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
213
 
131
214
  """
132
215
  return InstructionCollection(
133
- self.instruction_names_to_instructions, self.questions
216
+ self._instruction_names_to_instructions, self.questions
134
217
  )
135
218
 
136
- @staticmethod
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:
219
+ def _relevant_instructions(self, question: QuestionBase) -> dict:
213
220
  """This should be a dictionry with keys as question names and values as instructions that are relevant to the question.
214
221
 
215
222
  :param question: The question to get the relevant instructions for.
@@ -217,38 +224,13 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
217
224
  # Did the instruction come before the question and was it not modified by a change instruction?
218
225
 
219
226
  """
220
- return self.relevant_instructions_dict[question]
221
-
222
- @property
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:
227
+ return InstructionCollection(
228
+ self._instruction_names_to_instructions, self.questions
229
+ )[question]
242
230
 
243
- >>> s = Survey.example()
244
- >>> s.last_item_was_instruction
245
- False
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)
231
+ def show_flow(self, filename: Optional[str] = None) -> None:
232
+ """Show the flow of the survey."""
233
+ SurveyFlowVisualization(self).show_flow(filename=filename)
252
234
 
253
235
  def add_instruction(
254
236
  self, instruction: Union["Instruction", "ChangeInstruction"]
@@ -261,101 +243,21 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
261
243
  >>> from edsl import Instruction
262
244
  >>> i = Instruction(text="Pay attention to the following questions.", name="intro")
263
245
  >>> s = Survey().add_instruction(i)
264
- >>> s.instruction_names_to_instructions
246
+ >>> s._instruction_names_to_instructions
265
247
  {'intro': Instruction(name="intro", text="Pay attention to the following questions.")}
266
- >>> s.pseudo_indices
248
+ >>> s._pseudo_indices
267
249
  {'intro': -0.5}
268
250
  """
269
- import math
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
251
+ return EditSurvey(self).add_instruction(instruction)
288
252
 
289
253
  # endregion
290
-
291
- # region: Simulation methods
292
-
293
254
  @classmethod
294
- def random_survey(self):
295
- """Create a random survey."""
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
255
+ def random_survey(cls):
256
+ return Simulator.random_survey()
322
257
 
323
258
  def simulate(self) -> dict:
324
259
  """Simulate the survey and return the answers."""
325
- i = self.gen_path_through_survey()
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()
260
+ return Simulator(self).simulate()
359
261
 
360
262
  # endregion
361
263
 
@@ -391,26 +293,19 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
391
293
  )
392
294
  return self.question_name_to_index[question_name]
393
295
 
394
- def get(self, question_name: str) -> QuestionBase:
296
+ def _get_question_by_name(self, question_name: str) -> QuestionBase:
395
297
  """
396
298
  Return the question object given the question name.
397
299
 
398
300
  :param question_name: The name of the question to get.
399
301
 
400
302
  >>> s = Survey.example()
401
- >>> s.get_question("q0")
303
+ >>> s._get_question_by_name("q0")
402
304
  Question('multiple_choice', question_name = \"""q0\""", question_text = \"""Do you like school?\""", question_options = ['yes', 'no'])
403
305
  """
404
306
  if question_name not in self.question_name_to_index:
405
307
  raise SurveyError(f"Question name {question_name} not found in survey.")
406
- index = self.question_name_to_index[question_name]
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)
308
+ return self._questions[self.question_name_to_index[question_name]]
414
309
 
415
310
  def question_names_to_questions(self) -> dict:
416
311
  """Return a dictionary mapping question names to question attributes."""
@@ -443,12 +338,6 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
443
338
  # endregion
444
339
 
445
340
  # 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
341
  def to_dict(self, add_edsl_version=True) -> dict[str, Any]:
453
342
  """Serialize the Survey object to a dictionary.
454
343
 
@@ -456,10 +345,12 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
456
345
  >>> s.to_dict(add_edsl_version = False).keys()
457
346
  dict_keys(['questions', 'memory_plan', 'rule_collection', 'question_groups'])
458
347
  """
459
- return {
348
+ from edsl import __version__
349
+
350
+ d = {
460
351
  "questions": [
461
352
  q.to_dict(add_edsl_version=add_edsl_version)
462
- for q in self.recombined_questions_and_instructions()
353
+ for q in self._recombined_questions_and_instructions()
463
354
  ],
464
355
  "memory_plan": self.memory_plan.to_dict(add_edsl_version=add_edsl_version),
465
356
  "rule_collection": self.rule_collection.to_dict(
@@ -467,6 +358,13 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
467
358
  ),
468
359
  "question_groups": self.question_groups,
469
360
  }
361
+ if self.questions_to_randomize != []:
362
+ d["questions_to_randomize"] = self.questions_to_randomize
363
+
364
+ if add_edsl_version:
365
+ d["edsl_version"] = __version__
366
+ d["edsl_class_name"] = "Survey"
367
+ return d
470
368
 
471
369
  @classmethod
472
370
  @remove_edsl_version
@@ -489,6 +387,8 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
489
387
  """
490
388
 
491
389
  def get_class(pass_dict):
390
+ from edsl.questions.QuestionBase import QuestionBase
391
+
492
392
  if (class_name := pass_dict.get("edsl_class_name")) == "QuestionBase":
493
393
  return QuestionBase
494
394
  elif class_name == "Instruction":
@@ -508,11 +408,16 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
508
408
  get_class(q_dict).from_dict(q_dict) for q_dict in data["questions"]
509
409
  ]
510
410
  memory_plan = MemoryPlan.from_dict(data["memory_plan"])
411
+ if "questions_to_randomize" in data:
412
+ questions_to_randomize = data["questions_to_randomize"]
413
+ else:
414
+ questions_to_randomize = None
511
415
  survey = cls(
512
416
  questions=questions,
513
417
  memory_plan=memory_plan,
514
418
  rule_collection=RuleCollection.from_dict(data["rule_collection"]),
515
419
  question_groups=data["question_groups"],
420
+ questions_to_randomize=questions_to_randomize,
516
421
  )
517
422
  return survey
518
423
 
@@ -600,27 +505,16 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
600
505
 
601
506
  return Survey(questions=self.questions + other.questions)
602
507
 
603
- def move_question(self, identifier: Union[str, int], new_index: int):
604
- if isinstance(identifier, str):
605
- if identifier not in self.question_names:
606
- raise SurveyError(
607
- f"Question name '{identifier}' does not exist in the survey."
608
- )
609
- index = self.question_name_to_index[identifier]
610
- elif isinstance(identifier, int):
611
- if identifier < 0 or identifier >= len(self.questions):
612
- raise SurveyError(f"Index {identifier} is out of range.")
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
508
+ def move_question(self, identifier: Union[str, int], new_index: int) -> Survey:
509
+ """
510
+ >>> from edsl import QuestionMultipleChoice, Survey
511
+ >>> s = Survey.example()
512
+ >>> s.question_names
513
+ ['q0', 'q1', 'q2']
514
+ >>> s.move_question("q0", 2).question_names
515
+ ['q1', 'q2', 'q0']
516
+ """
517
+ return EditSurvey(self).move_question(identifier, new_index)
624
518
 
625
519
  def delete_question(self, identifier: Union[str, int]) -> Survey:
626
520
  """
@@ -640,54 +534,7 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
640
534
  >>> len(s.questions)
641
535
  0
642
536
  """
643
- if isinstance(identifier, str):
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
537
+ return EditSurvey(self).delete_question(identifier)
691
538
 
692
539
  def add_question(
693
540
  self, question: QuestionBase, index: Optional[int] = None
@@ -711,81 +558,17 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
711
558
  edsl.exceptions.surveys.SurveyCreationError: Question name 'q0' already exists in survey. Existing names are ['q0'].
712
559
  ...
713
560
  """
714
- if question.question_name in self.question_names:
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)
561
+ return EditSurvey(self).add_question(question, index)
720
562
 
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)
777
-
778
- return self
779
-
780
- def recombined_questions_and_instructions(
563
+ def _recombined_questions_and_instructions(
781
564
  self,
782
565
  ) -> list[Union[QuestionBase, "Instruction"]]:
783
566
  """Return a list of questions and instructions sorted by pseudo index."""
784
567
  questions_and_instructions = self._questions + list(
785
- self.instruction_names_to_instructions.values()
568
+ self._instruction_names_to_instructions.values()
786
569
  )
787
570
  return sorted(
788
- questions_and_instructions, key=lambda x: self.pseudo_indices[x.name]
571
+ questions_and_instructions, key=lambda x: self._pseudo_indices[x.name]
789
572
  )
790
573
 
791
574
  # endregion
@@ -797,7 +580,7 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
797
580
  >>> s = Survey.example().set_full_memory_mode()
798
581
 
799
582
  """
800
- self._set_memory_plan(lambda i: self.question_names[:i])
583
+ MemoryManagement(self)._set_memory_plan(lambda i: self.question_names[:i])
801
584
  return self
802
585
 
803
586
  def set_lagged_memory(self, lags: int) -> Survey:
@@ -805,10 +588,12 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
805
588
 
806
589
  The agent should remember the answers to the questions in the survey from the previous lags.
807
590
  """
808
- self._set_memory_plan(lambda i: self.question_names[max(0, i - lags) : i])
591
+ MemoryManagement(self)._set_memory_plan(
592
+ lambda i: self.question_names[max(0, i - lags) : i]
593
+ )
809
594
  return self
810
595
 
811
- def _set_memory_plan(self, prior_questions_func: Callable):
596
+ def _set_memory_plan(self, prior_questions_func: Callable) -> None:
812
597
  """Set memory plan based on a provided function determining prior questions.
813
598
 
814
599
  :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 +602,7 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
817
602
  >>> s._set_memory_plan(lambda i: s.question_names[:i])
818
603
 
819
604
  """
820
- for i, question_name in enumerate(self.question_names):
821
- self.memory_plan.add_memory_collection(
822
- focal_question=question_name,
823
- prior_questions=prior_questions_func(i),
824
- )
605
+ MemoryManagement(self)._set_memory_plan(prior_questions_func)
825
606
 
826
607
  def add_targeted_memory(
827
608
  self,
@@ -841,20 +622,10 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
841
622
 
842
623
  The agent should also remember the answers to prior_questions listed in prior_questions.
843
624
  """
844
- focal_question_name = self.question_names[
845
- self._get_question_index(focal_question)
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,
625
+ return MemoryManagement(self).add_targeted_memory(
626
+ focal_question, prior_question
854
627
  )
855
628
 
856
- return self
857
-
858
629
  def add_memory_collection(
859
630
  self,
860
631
  focal_question: Union[QuestionBase, str],
@@ -873,23 +644,9 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
873
644
  >>> s.memory_plan
874
645
  {'q2': Memory(prior_questions=['q0', 'q1'])}
875
646
  """
876
- focal_question_name = self.question_names[
877
- self._get_question_index(focal_question)
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
647
+ return MemoryManagement(self).add_memory_collection(
648
+ focal_question, prior_questions
887
649
  )
888
- return self
889
-
890
- # endregion
891
- # endregion
892
- # endregion
893
650
 
894
651
  # region: Question groups
895
652
  def add_question_group(
@@ -984,16 +741,9 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
984
741
 
985
742
  >>> s = Survey.example()
986
743
  >>> 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
- └───────────┴─────────────┴────────┴──────────┴─────────────┘
744
+ 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
745
  """
996
- self.rule_collection.show_rules()
746
+ return self.rule_collection.show_rules()
997
747
 
998
748
  def add_stop_rule(
999
749
  self, question: Union[QuestionBase, str], expression: str
@@ -1023,41 +773,15 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
1023
773
  edsl.exceptions.surveys.SurveyCreationError: The expression contains '<>', which is not allowed. You probably mean '!='.
1024
774
  ...
1025
775
  """
1026
- expression = ValidatedString(expression)
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
776
+ return RuleManager(self).add_stop_rule(question, expression)
1040
777
 
1041
778
  def clear_non_default_rules(self) -> Survey:
1042
779
  """Remove all non-default rules from the survey.
1043
780
 
1044
781
  >>> 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
- └───────────┴─────────────┴────────┴──────────┴─────────────┘
782
+ 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
783
  >>> 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
- └───────────┴────────────┴────────┴──────────┴─────────────┘
784
+ 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
785
  """
1062
786
  s = Survey()
1063
787
  for question in self.questions:
@@ -1088,38 +812,9 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
1088
812
 
1089
813
  """
1090
814
  question_index = self._get_question_index(question)
1091
- self._add_rule(question, expression, question_index + 1, before_rule=True)
1092
- return self
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
815
+ return RuleManager(self).add_rule(
816
+ question, expression, question_index + 1, before_rule=True
1121
817
  )
1122
- return new_priority
1123
818
 
1124
819
  def add_rule(
1125
820
  self,
@@ -1143,52 +838,10 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
1143
838
  'q2'
1144
839
 
1145
840
  """
1146
- return self._add_rule(
841
+ return RuleManager(self).add_rule(
1147
842
  question, expression, next_question, before_rule=before_rule
1148
843
  )
1149
844
 
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
845
  # endregion
1193
846
 
1194
847
  # region: Forward methods
@@ -1199,22 +852,26 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
1199
852
 
1200
853
  This takes the survey and adds an Agent and a Scenario via 'by' which converts to a Jobs object:
1201
854
 
1202
- >>> s = Survey.example(); from edsl import Agent; from edsl import Scenario
855
+ >>> s = Survey.example(); from edsl.agents import Agent; from edsl import Scenario
1203
856
  >>> s.by(Agent.example()).by(Scenario.example())
1204
857
  Jobs(...)
1205
858
  """
1206
859
  from edsl.jobs.Jobs import Jobs
1207
860
 
1208
- job = Jobs(survey=self)
1209
- return job.by(*args)
861
+ return Jobs(survey=self).by(*args)
1210
862
 
1211
863
  def to_jobs(self):
1212
- """Convert the survey to a Jobs object."""
864
+ """Convert the survey to a Jobs object.
865
+ >>> s = Survey.example()
866
+ >>> s.to_jobs()
867
+ Jobs(...)
868
+ """
1213
869
  from edsl.jobs.Jobs import Jobs
1214
870
 
1215
871
  return Jobs(survey=self)
1216
872
 
1217
873
  def show_prompts(self):
874
+ """Show the prompts for the survey."""
1218
875
  return self.to_jobs().show_prompts()
1219
876
 
1220
877
  # endregion
@@ -1226,6 +883,7 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
1226
883
  model=None,
1227
884
  agent=None,
1228
885
  cache=None,
886
+ verbose=False,
1229
887
  disable_remote_cache: bool = False,
1230
888
  disable_remote_inference: bool = False,
1231
889
  **kwargs,
@@ -1241,19 +899,21 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
1241
899
  >>> s(period = "evening", cache = False, disable_remote_cache = True, disable_remote_inference = True).select("answer.q0").first()
1242
900
  'no'
1243
901
  """
1244
- job = self.get_job(model, agent, **kwargs)
1245
- return job.run(
902
+
903
+ return self.get_job(model, agent, **kwargs).run(
1246
904
  cache=cache,
905
+ verbose=verbose,
1247
906
  disable_remote_cache=disable_remote_cache,
1248
907
  disable_remote_inference=disable_remote_inference,
1249
908
  )
1250
909
 
1251
910
  async def run_async(
1252
911
  self,
1253
- model: Optional["Model"] = None,
912
+ model: Optional["LanguageModel"] = None,
1254
913
  agent: Optional["Agent"] = None,
1255
914
  cache: Optional["Cache"] = None,
1256
915
  disable_remote_inference: bool = False,
916
+ disable_remote_cache: bool = False,
1257
917
  **kwargs,
1258
918
  ):
1259
919
  """Run the survey with default model, taking the required survey as arguments.
@@ -1263,7 +923,7 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
1263
923
  >>> def f(scenario, agent_traits): return "yes" if scenario["period"] == "morning" else "no"
1264
924
  >>> q = QuestionFunctional(question_name = "q0", func = f)
1265
925
  >>> s = Survey([q])
1266
- >>> async def test_run_async(): result = await s.run_async(period="morning", disable_remote_inference = True); print(result.select("answer.q0").first())
926
+ >>> async def test_run_async(): result = await s.run_async(period="morning", disable_remote_inference = True, disable_remote_cache=True); print(result.select("answer.q0").first())
1267
927
  >>> asyncio.run(test_run_async())
1268
928
  yes
1269
929
  >>> import asyncio
@@ -1271,20 +931,23 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
1271
931
  >>> def f(scenario, agent_traits): return "yes" if scenario["period"] == "morning" else "no"
1272
932
  >>> q = QuestionFunctional(question_name = "q0", func = f)
1273
933
  >>> s = Survey([q])
1274
- >>> async def test_run_async(): result = await s.run_async(period="evening", disable_remote_inference = True); print(result.select("answer.q0").first())
1275
- >>> asyncio.run(test_run_async())
934
+ >>> async def test_run_async(): result = await s.run_async(period="evening", disable_remote_inference = True, disable_remote_cache = True); print(result.select("answer.q0").first())
935
+ >>> results = asyncio.run(test_run_async())
1276
936
  no
1277
937
  """
1278
938
  # TODO: temp fix by creating a cache
1279
939
  if cache is None:
1280
940
  from edsl.data import Cache
1281
-
1282
941
  c = Cache()
1283
942
  else:
1284
943
  c = cache
1285
- jobs: "Jobs" = self.get_job(model=model, agent=agent, **kwargs)
944
+
945
+
946
+
947
+ jobs: "Jobs" = self.get_job(model=model, agent=agent, **kwargs).using(c)
1286
948
  return await jobs.run_async(
1287
- cache=c, disable_remote_inference=disable_remote_inference
949
+ disable_remote_inference=disable_remote_inference,
950
+ disable_remote_cache=disable_remote_cache,
1288
951
  )
1289
952
 
1290
953
  def run(self, *args, **kwargs) -> "Results":
@@ -1302,9 +965,30 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
1302
965
 
1303
966
  return Jobs(survey=self).run(*args, **kwargs)
1304
967
 
968
+ def using(self, obj: Union["Cache", "KeyLookup", "BucketCollection"]) -> "Jobs":
969
+ """Turn the survey into a Job and appends the arguments to the Job."""
970
+ from edsl.jobs.Jobs import Jobs
971
+
972
+ return Jobs(survey=self).using(obj)
973
+
974
+ def duplicate(self):
975
+ """Duplicate the survey.
976
+
977
+ >>> s = Survey.example()
978
+ >>> s2 = s.duplicate()
979
+ >>> s == s2
980
+ True
981
+ >>> s is s2
982
+ False
983
+
984
+ """
985
+ return Survey.from_dict(self.to_dict())
986
+
1305
987
  # region: Survey flow
1306
988
  def next_question(
1307
- self, current_question: Union[str, QuestionBase], answers: dict
989
+ self,
990
+ current_question: Optional[Union[str, QuestionBase]] = None,
991
+ answers: Optional[dict] = None,
1308
992
  ) -> Union[QuestionBase, EndOfSurvey.__class__]:
1309
993
  """
1310
994
  Return the next question in a survey.
@@ -1323,8 +1007,11 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
1323
1007
  'q1'
1324
1008
 
1325
1009
  """
1010
+ if current_question is None:
1011
+ return self.questions[0]
1012
+
1326
1013
  if isinstance(current_question, str):
1327
- current_question = self.get_question(current_question)
1014
+ current_question = self._get_question_by_name(current_question)
1328
1015
 
1329
1016
  question_index = self.question_name_to_index[current_question.question_name]
1330
1017
  next_question_object = self.rule_collection.next_question(
@@ -1354,14 +1041,7 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
1354
1041
 
1355
1042
  >>> s = Survey.example()
1356
1043
  >>> 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
- └───────────┴─────────────┴────────┴──────────┴─────────────┘
1044
+ 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
1045
 
1366
1046
  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
1047
 
@@ -1390,7 +1070,6 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
1390
1070
  question = self.next_question(question, self.answers)
1391
1071
 
1392
1072
  while not question == EndOfSurvey:
1393
- # breakpoint()
1394
1073
  answer = yield question
1395
1074
  self.answers.update(answer)
1396
1075
  # print(f"Answers: {self.answers}")
@@ -1399,69 +1078,6 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
1399
1078
 
1400
1079
  # endregion
1401
1080
 
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
1081
  def dag(self, textify: bool = False) -> DAG:
1466
1082
  """Return the DAG of the survey, which reflects both skip-logic and memory.
1467
1083
 
@@ -1473,14 +1089,9 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
1473
1089
  {1: {0}, 2: {0}}
1474
1090
 
1475
1091
  """
1476
- memory_dag = self.memory_plan.dag
1477
- rule_dag = self.rule_collection.dag
1478
- piping_dag = self.piping_dag
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
1092
+ from edsl.surveys.ConstructDAG import ConstructDAG
1093
+
1094
+ return ConstructDAG(self).dag(textify)
1484
1095
 
1485
1096
  ###################
1486
1097
  # DUNDER METHODS
@@ -1509,77 +1120,18 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
1509
1120
  elif isinstance(index, str):
1510
1121
  return getattr(self, index)
1511
1122
 
1512
- def _diff(self, other):
1513
- """Used for debugging. Print out the differences between two surveys."""
1514
- from rich import print
1515
-
1516
- for key, value in self.to_dict().items():
1517
- if value != other.to_dict()[key]:
1518
- print(f"Key: {key}")
1519
- print("\n")
1520
- print(f"Self: {value}")
1521
- print("\n")
1522
- print(f"Other: {other.to_dict()[key]}")
1523
- print("\n\n")
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."""
1123
+ # def _diff(self, other):
1124
+ # """Used for debugging. Print out the differences between two surveys."""
1125
+ # from rich import print
1547
1126
 
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()))
1127
+ # for key, value in self.to_dict().items():
1128
+ # if value != other.to_dict()[key]:
1129
+ # print(f"Key: {key}")
1130
+ # print("\n")
1131
+ # print(f"Self: {value}")
1132
+ # print("\n")
1133
+ # print(f"Other: {other.to_dict()[key]}")
1134
+ # print("\n\n")
1583
1135
 
1584
1136
  def __repr__(self) -> str:
1585
1137
  """Return a string representation of the survey."""
@@ -1587,60 +1139,20 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
1587
1139
  # questions_string = ", ".join([repr(q) for q in self._questions])
1588
1140
  questions_string = ", ".join([repr(q) for q in self.raw_passed_questions or []])
1589
1141
  # question_names_string = ", ".join([repr(name) for name in self.question_names])
1590
- return f"Survey(questions=[{questions_string}], memory_plan={self.memory_plan}, rule_collection={self.rule_collection}, question_groups={self.question_groups})"
1142
+ return f"Survey(questions=[{questions_string}], memory_plan={self.memory_plan}, rule_collection={self.rule_collection}, question_groups={self.question_groups}, questions_to_randomize={self.questions_to_randomize})"
1591
1143
 
1592
1144
  def _summary(self) -> dict:
1593
1145
  return {
1594
- "EDSL Class": "Survey",
1595
- "Number of Questions": len(self),
1596
- "Question Names": self.question_names,
1146
+ "# questions": len(self),
1147
+ "question_name list": self.question_names,
1597
1148
  }
1598
1149
 
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
1150
  def tree(self, node_list: Optional[List[str]] = None):
1604
1151
  return self.to_scenario_list().tree(node_list=node_list)
1605
1152
 
1606
1153
  def table(self, *fields, tablefmt=None) -> Table:
1607
1154
  return self.to_scenario_list().to_dataset().table(*fields, tablefmt=tablefmt)
1608
1155
 
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
1156
  # endregion
1645
1157
 
1646
1158
  def codebook(self) -> dict[str, str]:
@@ -1655,37 +1167,6 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
1655
1167
  codebook[question.question_name] = question.question_text
1656
1168
  return codebook
1657
1169
 
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
1170
  @classmethod
1690
1171
  def example(
1691
1172
  cls,
@@ -1744,7 +1225,7 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
1744
1225
 
1745
1226
  def get_job(self, model=None, agent=None, **kwargs):
1746
1227
  if model is None:
1747
- from edsl import Model
1228
+ from edsl.language_models.model import Model
1748
1229
 
1749
1230
  model = Model()
1750
1231
 
@@ -1753,7 +1234,7 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
1753
1234
  s = Scenario(kwargs)
1754
1235
 
1755
1236
  if not agent:
1756
- from edsl import Agent
1237
+ from edsl.agents.Agent import Agent
1757
1238
 
1758
1239
  agent = Agent()
1759
1240
 
@@ -1765,26 +1246,24 @@ def main():
1765
1246
 
1766
1247
  def example_survey():
1767
1248
  """Return an example survey."""
1768
- from edsl.questions.QuestionMultipleChoice import QuestionMultipleChoice
1769
- from edsl.surveys.Survey import Survey
1249
+ from edsl import QuestionMultipleChoice, QuestionList, QuestionNumerical, Survey
1770
1250
 
1771
1251
  q0 = QuestionMultipleChoice(
1772
- question_text="Do you like school?",
1773
- question_options=["yes", "no"],
1774
1252
  question_name="q0",
1253
+ question_text="What is the capital of France?",
1254
+ question_options=["London", "Paris", "Rome", "Boston", "I don't know"]
1775
1255
  )
1776
- q1 = QuestionMultipleChoice(
1777
- question_text="Why not?",
1778
- question_options=["killer bees in cafeteria", "other"],
1256
+ q1 = QuestionList(
1779
1257
  question_name="q1",
1258
+ question_text="Name some cities in France.",
1259
+ max_list_items = 5
1780
1260
  )
1781
- q2 = QuestionMultipleChoice(
1782
- question_text="Why?",
1783
- question_options=["**lack*** of killer bees in cafeteria", "other"],
1261
+ q2 = QuestionNumerical(
1784
1262
  question_name="q2",
1263
+ question_text="What is the population of {{ q0.answer }}?"
1785
1264
  )
1786
1265
  s = Survey(questions=[q0, q1, q2])
1787
- s = s.add_rule(q0, "q0 == 'yes'", q2)
1266
+ s = s.add_rule(q0, "q0 == 'Paris'", q2)
1788
1267
  return s
1789
1268
 
1790
1269
  s = example_survey()