edsl 0.1.33__py3-none-any.whl → 0.1.33.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. edsl/Base.py +3 -9
  2. edsl/__init__.py +3 -8
  3. edsl/__version__.py +1 -1
  4. edsl/agents/Agent.py +8 -40
  5. edsl/agents/AgentList.py +0 -43
  6. edsl/agents/Invigilator.py +219 -135
  7. edsl/agents/InvigilatorBase.py +59 -148
  8. edsl/agents/{PromptConstructor.py → PromptConstructionMixin.py} +89 -138
  9. edsl/agents/__init__.py +0 -1
  10. edsl/config.py +56 -47
  11. edsl/coop/coop.py +7 -50
  12. edsl/data/Cache.py +1 -35
  13. edsl/data_transfer_models.py +38 -73
  14. edsl/enums.py +0 -4
  15. edsl/exceptions/language_models.py +1 -25
  16. edsl/exceptions/questions.py +5 -62
  17. edsl/exceptions/results.py +0 -4
  18. edsl/inference_services/AnthropicService.py +11 -13
  19. edsl/inference_services/AwsBedrock.py +17 -19
  20. edsl/inference_services/AzureAI.py +20 -37
  21. edsl/inference_services/GoogleService.py +12 -16
  22. edsl/inference_services/GroqService.py +0 -2
  23. edsl/inference_services/InferenceServiceABC.py +3 -58
  24. edsl/inference_services/OpenAIService.py +54 -48
  25. edsl/inference_services/models_available_cache.py +6 -0
  26. edsl/inference_services/registry.py +0 -6
  27. edsl/jobs/Answers.py +12 -10
  28. edsl/jobs/Jobs.py +21 -36
  29. edsl/jobs/buckets/BucketCollection.py +15 -24
  30. edsl/jobs/buckets/TokenBucket.py +14 -93
  31. edsl/jobs/interviews/Interview.py +78 -366
  32. edsl/jobs/interviews/InterviewExceptionEntry.py +19 -85
  33. edsl/jobs/interviews/InterviewTaskBuildingMixin.py +286 -0
  34. edsl/jobs/interviews/{InterviewExceptionCollection.py → interview_exception_tracking.py} +68 -14
  35. edsl/jobs/interviews/retry_management.py +37 -0
  36. edsl/jobs/runners/JobsRunnerAsyncio.py +175 -146
  37. edsl/jobs/runners/JobsRunnerStatusMixin.py +333 -0
  38. edsl/jobs/tasks/QuestionTaskCreator.py +23 -30
  39. edsl/jobs/tasks/TaskHistory.py +213 -148
  40. edsl/language_models/LanguageModel.py +156 -261
  41. edsl/language_models/ModelList.py +2 -2
  42. edsl/language_models/RegisterLanguageModelsMeta.py +29 -14
  43. edsl/language_models/registry.py +6 -23
  44. edsl/language_models/repair.py +19 -0
  45. edsl/prompts/Prompt.py +2 -52
  46. edsl/questions/AnswerValidatorMixin.py +26 -23
  47. edsl/questions/QuestionBase.py +249 -329
  48. edsl/questions/QuestionBudget.py +41 -99
  49. edsl/questions/QuestionCheckBox.py +35 -227
  50. edsl/questions/QuestionExtract.py +27 -98
  51. edsl/questions/QuestionFreeText.py +29 -52
  52. edsl/questions/QuestionFunctional.py +0 -7
  53. edsl/questions/QuestionList.py +22 -141
  54. edsl/questions/QuestionMultipleChoice.py +65 -159
  55. edsl/questions/QuestionNumerical.py +46 -88
  56. edsl/questions/QuestionRank.py +24 -182
  57. edsl/questions/RegisterQuestionsMeta.py +12 -31
  58. edsl/questions/__init__.py +4 -3
  59. edsl/questions/derived/QuestionLikertFive.py +5 -10
  60. edsl/questions/derived/QuestionLinearScale.py +2 -15
  61. edsl/questions/derived/QuestionTopK.py +1 -10
  62. edsl/questions/derived/QuestionYesNo.py +3 -24
  63. edsl/questions/descriptors.py +7 -43
  64. edsl/questions/question_registry.py +2 -6
  65. edsl/results/Dataset.py +0 -20
  66. edsl/results/DatasetExportMixin.py +48 -46
  67. edsl/results/Result.py +5 -32
  68. edsl/results/Results.py +46 -135
  69. edsl/results/ResultsDBMixin.py +3 -3
  70. edsl/scenarios/FileStore.py +10 -71
  71. edsl/scenarios/Scenario.py +25 -96
  72. edsl/scenarios/ScenarioImageMixin.py +2 -2
  73. edsl/scenarios/ScenarioList.py +39 -361
  74. edsl/scenarios/ScenarioListExportMixin.py +0 -9
  75. edsl/scenarios/ScenarioListPdfMixin.py +4 -150
  76. edsl/study/SnapShot.py +1 -8
  77. edsl/study/Study.py +0 -32
  78. edsl/surveys/Rule.py +1 -10
  79. edsl/surveys/RuleCollection.py +5 -21
  80. edsl/surveys/Survey.py +310 -636
  81. edsl/surveys/SurveyExportMixin.py +9 -71
  82. edsl/surveys/SurveyFlowVisualizationMixin.py +1 -2
  83. edsl/surveys/SurveyQualtricsImport.py +4 -75
  84. edsl/utilities/gcp_bucket/simple_example.py +9 -0
  85. edsl/utilities/utilities.py +1 -9
  86. {edsl-0.1.33.dist-info → edsl-0.1.33.dev1.dist-info}/METADATA +2 -5
  87. edsl-0.1.33.dev1.dist-info/RECORD +209 -0
  88. edsl/TemplateLoader.py +0 -24
  89. edsl/auto/AutoStudy.py +0 -117
  90. edsl/auto/StageBase.py +0 -230
  91. edsl/auto/StageGenerateSurvey.py +0 -178
  92. edsl/auto/StageLabelQuestions.py +0 -125
  93. edsl/auto/StagePersona.py +0 -61
  94. edsl/auto/StagePersonaDimensionValueRanges.py +0 -88
  95. edsl/auto/StagePersonaDimensionValues.py +0 -74
  96. edsl/auto/StagePersonaDimensions.py +0 -69
  97. edsl/auto/StageQuestions.py +0 -73
  98. edsl/auto/SurveyCreatorPipeline.py +0 -21
  99. edsl/auto/utilities.py +0 -224
  100. edsl/coop/PriceFetcher.py +0 -58
  101. edsl/inference_services/MistralAIService.py +0 -120
  102. edsl/inference_services/TestService.py +0 -80
  103. edsl/inference_services/TogetherAIService.py +0 -170
  104. edsl/jobs/FailedQuestion.py +0 -78
  105. edsl/jobs/runners/JobsRunnerStatus.py +0 -331
  106. edsl/language_models/fake_openai_call.py +0 -15
  107. edsl/language_models/fake_openai_service.py +0 -61
  108. edsl/language_models/utilities.py +0 -61
  109. edsl/questions/QuestionBaseGenMixin.py +0 -133
  110. edsl/questions/QuestionBasePromptsMixin.py +0 -266
  111. edsl/questions/Quick.py +0 -41
  112. edsl/questions/ResponseValidatorABC.py +0 -170
  113. edsl/questions/decorators.py +0 -21
  114. edsl/questions/prompt_templates/question_budget.jinja +0 -13
  115. edsl/questions/prompt_templates/question_checkbox.jinja +0 -32
  116. edsl/questions/prompt_templates/question_extract.jinja +0 -11
  117. edsl/questions/prompt_templates/question_free_text.jinja +0 -3
  118. edsl/questions/prompt_templates/question_linear_scale.jinja +0 -11
  119. edsl/questions/prompt_templates/question_list.jinja +0 -17
  120. edsl/questions/prompt_templates/question_multiple_choice.jinja +0 -33
  121. edsl/questions/prompt_templates/question_numerical.jinja +0 -37
  122. edsl/questions/templates/__init__.py +0 -0
  123. edsl/questions/templates/budget/__init__.py +0 -0
  124. edsl/questions/templates/budget/answering_instructions.jinja +0 -7
  125. edsl/questions/templates/budget/question_presentation.jinja +0 -7
  126. edsl/questions/templates/checkbox/__init__.py +0 -0
  127. edsl/questions/templates/checkbox/answering_instructions.jinja +0 -10
  128. edsl/questions/templates/checkbox/question_presentation.jinja +0 -22
  129. edsl/questions/templates/extract/__init__.py +0 -0
  130. edsl/questions/templates/extract/answering_instructions.jinja +0 -7
  131. edsl/questions/templates/extract/question_presentation.jinja +0 -1
  132. edsl/questions/templates/free_text/__init__.py +0 -0
  133. edsl/questions/templates/free_text/answering_instructions.jinja +0 -0
  134. edsl/questions/templates/free_text/question_presentation.jinja +0 -1
  135. edsl/questions/templates/likert_five/__init__.py +0 -0
  136. edsl/questions/templates/likert_five/answering_instructions.jinja +0 -10
  137. edsl/questions/templates/likert_five/question_presentation.jinja +0 -12
  138. edsl/questions/templates/linear_scale/__init__.py +0 -0
  139. edsl/questions/templates/linear_scale/answering_instructions.jinja +0 -5
  140. edsl/questions/templates/linear_scale/question_presentation.jinja +0 -5
  141. edsl/questions/templates/list/__init__.py +0 -0
  142. edsl/questions/templates/list/answering_instructions.jinja +0 -4
  143. edsl/questions/templates/list/question_presentation.jinja +0 -5
  144. edsl/questions/templates/multiple_choice/__init__.py +0 -0
  145. edsl/questions/templates/multiple_choice/answering_instructions.jinja +0 -9
  146. edsl/questions/templates/multiple_choice/html.jinja +0 -0
  147. edsl/questions/templates/multiple_choice/question_presentation.jinja +0 -12
  148. edsl/questions/templates/numerical/__init__.py +0 -0
  149. edsl/questions/templates/numerical/answering_instructions.jinja +0 -8
  150. edsl/questions/templates/numerical/question_presentation.jinja +0 -7
  151. edsl/questions/templates/rank/__init__.py +0 -0
  152. edsl/questions/templates/rank/answering_instructions.jinja +0 -11
  153. edsl/questions/templates/rank/question_presentation.jinja +0 -15
  154. edsl/questions/templates/top_k/__init__.py +0 -0
  155. edsl/questions/templates/top_k/answering_instructions.jinja +0 -8
  156. edsl/questions/templates/top_k/question_presentation.jinja +0 -22
  157. edsl/questions/templates/yes_no/__init__.py +0 -0
  158. edsl/questions/templates/yes_no/answering_instructions.jinja +0 -6
  159. edsl/questions/templates/yes_no/question_presentation.jinja +0 -12
  160. edsl/results/DatasetTree.py +0 -145
  161. edsl/results/Selector.py +0 -118
  162. edsl/results/tree_explore.py +0 -115
  163. edsl/surveys/instructions/ChangeInstruction.py +0 -47
  164. edsl/surveys/instructions/Instruction.py +0 -34
  165. edsl/surveys/instructions/InstructionCollection.py +0 -77
  166. edsl/surveys/instructions/__init__.py +0 -0
  167. edsl/templates/error_reporting/base.html +0 -24
  168. edsl/templates/error_reporting/exceptions_by_model.html +0 -35
  169. edsl/templates/error_reporting/exceptions_by_question_name.html +0 -17
  170. edsl/templates/error_reporting/exceptions_by_type.html +0 -17
  171. edsl/templates/error_reporting/interview_details.html +0 -116
  172. edsl/templates/error_reporting/interviews.html +0 -10
  173. edsl/templates/error_reporting/overview.html +0 -5
  174. edsl/templates/error_reporting/performance_plot.html +0 -2
  175. edsl/templates/error_reporting/report.css +0 -74
  176. edsl/templates/error_reporting/report.html +0 -118
  177. edsl/templates/error_reporting/report.js +0 -25
  178. edsl-0.1.33.dist-info/RECORD +0 -295
  179. {edsl-0.1.33.dist-info → edsl-0.1.33.dev1.dist-info}/LICENSE +0 -0
  180. {edsl-0.1.33.dist-info → edsl-0.1.33.dev1.dist-info}/WHEEL +0 -0
edsl/surveys/Survey.py CHANGED
@@ -20,24 +20,6 @@ from edsl.surveys.SurveyExportMixin import SurveyExportMixin
20
20
  from edsl.surveys.SurveyFlowVisualizationMixin import SurveyFlowVisualizationMixin
21
21
  from edsl.utilities.decorators import add_edsl_version, remove_edsl_version
22
22
 
23
- from edsl.agents.Agent import Agent
24
-
25
-
26
- class ValidatedString(str):
27
- def __new__(cls, content):
28
- if "<>" in content:
29
- raise ValueError(
30
- "The expression contains '<>', which is not allowed. You probably mean '!='."
31
- )
32
- return super().__new__(cls, content)
33
-
34
-
35
- # from edsl.surveys.Instruction import Instruction
36
- # from edsl.surveys.Instruction import ChangeInstruction
37
- from edsl.surveys.instructions.InstructionCollection import InstructionCollection
38
- from edsl.surveys.instructions.Instruction import Instruction
39
- from edsl.surveys.instructions.ChangeInstruction import ChangeInstruction
40
-
41
23
 
42
24
  class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
43
25
  """A collection of questions that supports skip logic."""
@@ -60,13 +42,11 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
60
42
 
61
43
  def __init__(
62
44
  self,
63
- questions: Optional[
64
- list[Union[QuestionBase, Instruction, ChangeInstruction]]
65
- ] = None,
45
+ questions: Optional[list[QuestionBase]] = None,
66
46
  memory_plan: Optional[MemoryPlan] = None,
67
47
  rule_collection: Optional[RuleCollection] = None,
68
48
  question_groups: Optional[dict[str, tuple[int, int]]] = None,
69
- name: Optional[str] = None,
49
+ name: str = None,
70
50
  ):
71
51
  """Create a new survey.
72
52
 
@@ -76,33 +56,13 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
76
56
  :param question_groups: The groups of questions in the survey.
77
57
  :param name: The name of the survey - DEPRECATED.
78
58
 
79
-
80
- >>> from edsl import QuestionFreeText
81
- >>> q1 = QuestionFreeText(question_text = "What is your name?", question_name = "name")
82
- >>> q2 = QuestionFreeText(question_text = "What is your favorite color?", question_name = "color")
83
- >>> q3 = QuestionFreeText(question_text = "Is a hot dog a sandwich", question_name = "food")
84
- >>> s = Survey([q1, q2, q3], question_groups = {"demographics": (0, 1), "substantive":(3)})
85
-
86
-
87
59
  """
88
-
89
- self.raw_passed_questions = questions
90
-
91
- (
92
- true_questions,
93
- instruction_names_to_instructions,
94
- self.pseudo_indices,
95
- ) = self._separate_questions_and_instructions(questions or [])
96
-
97
60
  self.rule_collection = RuleCollection(
98
- num_questions=len(true_questions) if true_questions else None
61
+ num_questions=len(questions) if questions else None
99
62
  )
100
63
  # the RuleCollection needs to be present while we add the questions; we might override this later
101
64
  # if a rule_collection is provided. This allows us to serialize the survey with the rule_collection.
102
-
103
- self.questions = true_questions
104
- self.instruction_names_to_instructions = instruction_names_to_instructions
105
-
65
+ self.questions = questions or []
106
66
  self.memory_plan = memory_plan or MemoryPlan(self)
107
67
  if question_groups is not None:
108
68
  self.question_groups = question_groups
@@ -118,177 +78,6 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
118
78
 
119
79
  warnings.warn("name parameter to a survey is deprecated.")
120
80
 
121
- # region: Suvry instruction handling
122
- @property
123
- def relevant_instructions_dict(self) -> InstructionCollection:
124
- """Return a dictionary with keys as question names and values as instructions that are relevant to the question.
125
-
126
- >>> s = Survey.example(include_instructions=True)
127
- >>> s.relevant_instructions_dict
128
- {'q0': [Instruction(name="attention", text="Please pay attention!")], 'q1': [Instruction(name="attention", text="Please pay attention!")], 'q2': [Instruction(name="attention", text="Please pay attention!")]}
129
-
130
- """
131
- return InstructionCollection(
132
- self.instruction_names_to_instructions, self.questions
133
- )
134
-
135
- @staticmethod
136
- def _separate_questions_and_instructions(questions_and_instructions: list) -> tuple:
137
- """
138
- The 'pseudo_indices' attribute is a dictionary that maps question names to pseudo-indices
139
- that are used to order questions and instructions in the survey.
140
- Only questions get real indices; instructions get pseudo-indices.
141
- However, the order of the pseudo-indices is the same as the order questions and instructions are added to the survey.
142
-
143
- We don't have to know how many instructions there are to calculate the pseudo-indices because they are
144
- calculated by the inverse of one minus the sum of 1/2^n for n in the number of instructions run so far.
145
-
146
- >>> from edsl import Instruction
147
- >>> i = Instruction(text = "Pay attention to the following questions.", name = "intro")
148
- >>> i2 = Instruction(text = "How are you feeling today?", name = "followon_intro")
149
- >>> from edsl import QuestionFreeText; q1 = QuestionFreeText.example()
150
- >>> from edsl import QuestionMultipleChoice; q2 = QuestionMultipleChoice.example()
151
- >>> s = Survey([q1, i, i2, q2])
152
- >>> len(s.instruction_names_to_instructions)
153
- 2
154
- >>> s.pseudo_indices
155
- {'how_are_you': 0, 'intro': 0.5, 'followon_intro': 0.75, 'how_feeling': 1}
156
-
157
- >>> from edsl import ChangeInstruction
158
- >>> q3 = QuestionFreeText(question_text = "What is your favorite color?", question_name = "color")
159
- >>> i_change = ChangeInstruction(drop = ["intro"])
160
- >>> s = Survey([q1, i, q2, i_change, q3])
161
- >>> [i.name for i in s.relevant_instructions(q1)]
162
- []
163
- >>> [i.name for i in s.relevant_instructions(q2)]
164
- ['intro']
165
- >>> [i.name for i in s.relevant_instructions(q3)]
166
- []
167
-
168
- >>> i_change = ChangeInstruction(keep = ["poop"], drop = [])
169
- >>> s = Survey([q1, i, q2, i_change])
170
- Traceback (most recent call last):
171
- ...
172
- ValueError: ChangeInstruction change_instruction_0 references instruction poop which does not exist.
173
- """
174
- from edsl.surveys.instructions.Instruction import Instruction
175
- from edsl.surveys.instructions.ChangeInstruction import ChangeInstruction
176
-
177
- true_questions = []
178
- instruction_names_to_instructions = {}
179
-
180
- num_change_instructions = 0
181
- pseudo_indices = {}
182
- instructions_run_length = 0
183
- for entry in questions_and_instructions:
184
- if isinstance(entry, Instruction) or isinstance(entry, ChangeInstruction):
185
- if isinstance(entry, ChangeInstruction):
186
- entry.add_name(num_change_instructions)
187
- num_change_instructions += 1
188
- for prior_instruction in entry.keep + entry.drop:
189
- if prior_instruction not in instruction_names_to_instructions:
190
- raise ValueError(
191
- f"ChangeInstruction {entry.name} references instruction {prior_instruction} which does not exist."
192
- )
193
- instructions_run_length += 1
194
- delta = 1 - 1.0 / (2.0**instructions_run_length)
195
- pseudo_index = (len(true_questions) - 1) + delta
196
- entry.pseudo_index = pseudo_index
197
- instruction_names_to_instructions[entry.name] = entry
198
- elif isinstance(entry, QuestionBase):
199
- pseudo_index = len(true_questions)
200
- instructions_run_length = 0
201
- true_questions.append(entry)
202
- else:
203
- raise ValueError(
204
- f"Entry {repr(entry)} is not a QuestionBase or an Instruction."
205
- )
206
-
207
- pseudo_indices[entry.name] = pseudo_index
208
-
209
- return true_questions, instruction_names_to_instructions, pseudo_indices
210
-
211
- def relevant_instructions(self, question) -> dict:
212
- """This should be a dictionry with keys as question names and values as instructions that are relevant to the question.
213
-
214
- :param question: The question to get the relevant instructions for.
215
-
216
- # Did the instruction come before the question and was it not modified by a change instruction?
217
-
218
- """
219
- return self.relevant_instructions_dict[question]
220
-
221
- @property
222
- def max_pseudo_index(self) -> float:
223
- """Return the maximum pseudo index in the survey.
224
-
225
- Example:
226
-
227
- >>> s = Survey.example()
228
- >>> s.max_pseudo_index
229
- 2
230
- """
231
- if len(self.pseudo_indices) == 0:
232
- return -1
233
- return max(self.pseudo_indices.values())
234
-
235
- @property
236
- def last_item_was_instruction(self) -> bool:
237
- """Return whether the last item added to the survey was an instruction.
238
- This is used to determine the pseudo-index of the next item added to the survey.
239
-
240
- Example:
241
-
242
- >>> s = Survey.example()
243
- >>> s.last_item_was_instruction
244
- False
245
- >>> from edsl.surveys.instructions.Instruction import Instruction
246
- >>> s = s.add_instruction(Instruction(text="Pay attention to the following questions.", name="intro"))
247
- >>> s.last_item_was_instruction
248
- True
249
- """
250
- return isinstance(self.max_pseudo_index, float)
251
-
252
- def add_instruction(
253
- self, instruction: Union["Instruction", "ChangeInstruction"]
254
- ) -> Survey:
255
- """
256
- Add an instruction to the survey.
257
-
258
- :param instruction: The instruction to add to the survey.
259
-
260
- >>> from edsl import Instruction
261
- >>> i = Instruction(text="Pay attention to the following questions.", name="intro")
262
- >>> s = Survey().add_instruction(i)
263
- >>> s.instruction_names_to_instructions
264
- {'intro': Instruction(name="intro", text="Pay attention to the following questions.")}
265
- >>> s.pseudo_indices
266
- {'intro': -0.5}
267
- """
268
- import math
269
-
270
- if instruction.name in self.instruction_names_to_instructions:
271
- raise SurveyCreationError(
272
- f"""Instruction name '{instruction.name}' already exists in survey. Existing names are {self.instruction_names_to_instructions.keys()}."""
273
- )
274
- self.instruction_names_to_instructions[instruction.name] = instruction
275
-
276
- # was the last thing added an instruction or a question?
277
- if self.last_item_was_instruction:
278
- pseudo_index = (
279
- self.max_pseudo_index
280
- + (math.ceil(self.max_pseudo_index) - self.max_pseudo_index) / 2
281
- )
282
- else:
283
- pseudo_index = self.max_pseudo_index + 1.0 / 2.0
284
- self.pseudo_indices[instruction.name] = pseudo_index
285
-
286
- return self
287
-
288
- # endregion
289
-
290
- # region: Simulation methods
291
-
292
81
  def simulate(self) -> dict:
293
82
  """Simulate the survey and return the answers."""
294
83
  i = self.gen_path_through_survey()
@@ -304,6 +93,9 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
304
93
  def create_agent(self) -> "Agent":
305
94
  """Create an agent from the simulated answers."""
306
95
  answers_dict = self.simulate()
96
+ from edsl.agents.Agent import Agent
97
+
98
+ a = Agent(traits=answers_dict)
307
99
 
308
100
  def construct_answer_dict_function(traits: dict) -> Callable:
309
101
  def func(self, question: "QuestionBase", scenario=None):
@@ -311,48 +103,16 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
311
103
 
312
104
  return func
313
105
 
314
- return Agent(traits=answers_dict).add_direct_question_answering_method(
106
+ a.add_direct_question_answering_method(
315
107
  construct_answer_dict_function(answers_dict)
316
108
  )
109
+ return a
317
110
 
318
111
  def simulate_results(self) -> "Results":
319
112
  """Simulate the survey and return the results."""
320
113
  a = self.create_agent()
321
114
  return self.by([a]).run()
322
115
 
323
- # endregion
324
-
325
- # region: Access methods
326
- def _get_question_index(
327
- self, q: Union[QuestionBase, str, EndOfSurvey.__class__]
328
- ) -> Union[int, EndOfSurvey.__class__]:
329
- """Return the index of the question or EndOfSurvey object.
330
-
331
- :param q: The question or question name to get the index of.
332
-
333
- It can handle it if the user passes in the question name, the question object, or the EndOfSurvey object.
334
-
335
- >>> s = Survey.example()
336
- >>> s._get_question_index("q0")
337
- 0
338
-
339
- This doesnt' work with questions that don't exist:
340
-
341
- >>> s._get_question_index("poop")
342
- Traceback (most recent call last):
343
- ...
344
- ValueError: Question name poop not found in survey. The current question names are {'q0': 0, 'q1': 1, 'q2': 2}.
345
- """
346
- if q == EndOfSurvey:
347
- return EndOfSurvey
348
- else:
349
- question_name = q if isinstance(q, str) else q.question_name
350
- if question_name not in self.question_name_to_index:
351
- raise ValueError(
352
- f"""Question name {question_name} not found in survey. The current question names are {self.question_name_to_index}."""
353
- )
354
- return self.question_name_to_index[question_name]
355
-
356
116
  def get(self, question_name: str) -> QuestionBase:
357
117
  """
358
118
  Return the question object given the question name.
@@ -368,184 +128,22 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
368
128
  index = self.question_name_to_index[question_name]
369
129
  return self._questions[index]
370
130
 
131
+ def question_names_to_questions(self) -> dict:
132
+ """Return a dictionary mapping question names to question attributes."""
133
+ return {q.question_name: q for q in self.questions}
134
+
371
135
  def get_question(self, question_name: str) -> QuestionBase:
372
136
  """Return the question object given the question name."""
373
137
  # import warnings
374
138
  # warnings.warn("survey.get_question is deprecated. Use subscript operator instead.")
375
139
  return self.get(question_name)
376
140
 
377
- def question_names_to_questions(self) -> dict:
378
- """Return a dictionary mapping question names to question attributes."""
379
- return {q.question_name: q for q in self.questions}
380
-
381
- @property
382
- def question_names(self) -> list[str]:
383
- """Return a list of question names in the survey.
384
-
385
- Example:
386
-
387
- >>> s = Survey.example()
388
- >>> s.question_names
389
- ['q0', 'q1', 'q2']
390
- """
391
- # return list(self.question_name_to_index.keys())
392
- return [q.question_name for q in self.questions]
393
-
394
- @property
395
- def question_name_to_index(self) -> dict[str, int]:
396
- """Return a dictionary mapping question names to question indices.
397
-
398
- Example:
399
-
400
- >>> s = Survey.example()
401
- >>> s.question_name_to_index
402
- {'q0': 0, 'q1': 1, 'q2': 2}
403
- """
404
- return {q.question_name: i for i, q in enumerate(self.questions)}
405
-
406
- # endregion
407
-
408
- # region: serialization methods
409
141
  def __hash__(self) -> int:
410
142
  """Return a hash of the question."""
411
143
  from edsl.utilities.utilities import dict_hash
412
144
 
413
145
  return dict_hash(self._to_dict())
414
146
 
415
- def _to_dict(self) -> dict[str, Any]:
416
- """Serialize the Survey object to a dictionary.
417
-
418
- >>> s = Survey.example()
419
- >>> s._to_dict().keys()
420
- dict_keys(['questions', 'memory_plan', 'rule_collection', 'question_groups'])
421
- """
422
- return {
423
- "questions": [
424
- q._to_dict() for q in self.recombined_questions_and_instructions()
425
- ],
426
- "memory_plan": self.memory_plan.to_dict(),
427
- "rule_collection": self.rule_collection.to_dict(),
428
- "question_groups": self.question_groups,
429
- }
430
-
431
- @add_edsl_version
432
- def to_dict(self) -> dict[str, Any]:
433
- """Serialize the Survey object to a dictionary.
434
-
435
- >>> s = Survey.example()
436
- >>> s.to_dict().keys()
437
- dict_keys(['questions', 'memory_plan', 'rule_collection', 'question_groups', 'edsl_version', 'edsl_class_name'])
438
-
439
- """
440
- return self._to_dict()
441
-
442
- @classmethod
443
- @remove_edsl_version
444
- def from_dict(cls, data: dict) -> Survey:
445
- """Deserialize the dictionary back to a Survey object.
446
-
447
- :param data: The dictionary to deserialize.
448
-
449
- >>> d = Survey.example().to_dict()
450
- >>> s = Survey.from_dict(d)
451
- >>> s == Survey.example()
452
- True
453
-
454
- >>> s = Survey.example(include_instructions = True)
455
- >>> d = s.to_dict()
456
- >>> news = Survey.from_dict(d)
457
- >>> news == s
458
- True
459
-
460
- """
461
-
462
- def get_class(pass_dict):
463
- if (class_name := pass_dict.get("edsl_class_name")) == "QuestionBase":
464
- return QuestionBase
465
- elif class_name == "Instruction":
466
- from edsl.surveys.instructions.Instruction import Instruction
467
-
468
- return Instruction
469
- elif class_name == "ChangeInstruction":
470
- from edsl.surveys.instructions.ChangeInstruction import (
471
- ChangeInstruction,
472
- )
473
-
474
- return ChangeInstruction
475
- else:
476
- # some data might not have the edsl_class_name
477
- return QuestionBase
478
- # raise ValueError(f"Class {pass_dict['edsl_class_name']} not found")
479
-
480
- questions = [
481
- get_class(q_dict).from_dict(q_dict) for q_dict in data["questions"]
482
- ]
483
- memory_plan = MemoryPlan.from_dict(data["memory_plan"])
484
- survey = cls(
485
- questions=questions,
486
- memory_plan=memory_plan,
487
- rule_collection=RuleCollection.from_dict(data["rule_collection"]),
488
- question_groups=data["question_groups"],
489
- )
490
- return survey
491
-
492
- # endregion
493
-
494
- # region: Survey template parameters
495
- @property
496
- def scenario_attributes(self) -> list[str]:
497
- """Return a list of attributes that admissible Scenarios should have.
498
-
499
- Here we have a survey with a question that uses a jinja2 style {{ }} template:
500
-
501
- >>> from edsl import QuestionFreeText
502
- >>> s = Survey().add_question(QuestionFreeText(question_text="{{ greeting }}. What is your name?", question_name="name"))
503
- >>> s.scenario_attributes
504
- ['greeting']
505
-
506
- >>> s = Survey().add_question(QuestionFreeText(question_text="{{ greeting }}. What is your {{ attribute }}?", question_name="name"))
507
- >>> s.scenario_attributes
508
- ['greeting', 'attribute']
509
-
510
-
511
- """
512
- temp = []
513
- for question in self.questions:
514
- question_text = question.question_text
515
- # extract the contents of all {{ }} in the question text using regex
516
- matches = re.findall(r"\{\{(.+?)\}\}", question_text)
517
- # remove whitespace
518
- matches = [match.strip() for match in matches]
519
- # add them to the temp list
520
- temp.extend(matches)
521
- return temp
522
-
523
- @property
524
- def parameters(self):
525
- """Return a set of parameters in the survey.
526
-
527
- >>> s = Survey.example()
528
- >>> s.parameters
529
- set()
530
- """
531
- return set.union(*[q.parameters for q in self.questions])
532
-
533
- @property
534
- def parameters_by_question(self):
535
- """Return a dictionary of parameters by question in the survey.
536
- >>> from edsl import QuestionFreeText
537
- >>> q = QuestionFreeText(question_name = "example", question_text = "What is the capital of {{ country}}?")
538
- >>> s = Survey([q])
539
- >>> s.parameters_by_question
540
- {'example': {'country'}}
541
- """
542
- return {q.question_name: q.parameters for q in self.questions}
543
-
544
- # endregion
545
-
546
- # region: Survey construction
547
-
548
- # region: Adding questions and combining surveys
549
147
  def __add__(self, other: Survey) -> Survey:
550
148
  """Combine two surveys.
551
149
 
@@ -573,6 +171,51 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
573
171
 
574
172
  return Survey(questions=self.questions + other.questions)
575
173
 
174
+ def clear_non_default_rules(self) -> Survey:
175
+ s = Survey()
176
+ for question in self.questions:
177
+ s.add_question(question)
178
+ return s
179
+
180
+ @property
181
+ def parameters(self):
182
+ """Return a set of parameters in the survey.
183
+
184
+ >>> s = Survey.example()
185
+ >>> s.parameters
186
+ set()
187
+ """
188
+ return set.union(*[q.parameters for q in self.questions])
189
+
190
+ @property
191
+ def parameters_by_question(self):
192
+ return {q.question_name: q.parameters for q in self.questions}
193
+
194
+ @property
195
+ def question_names(self) -> list[str]:
196
+ """Return a list of question names in the survey.
197
+
198
+ Example:
199
+
200
+ >>> s = Survey.example()
201
+ >>> s.question_names
202
+ ['q0', 'q1', 'q2']
203
+ """
204
+ # return list(self.question_name_to_index.keys())
205
+ return [q.question_name for q in self.questions]
206
+
207
+ @property
208
+ def question_name_to_index(self) -> dict[str, int]:
209
+ """Return a dictionary mapping question names to question indices.
210
+
211
+ Example:
212
+
213
+ >>> s = Survey.example()
214
+ >>> s.question_name_to_index
215
+ {'q0': 0, 'q1': 1, 'q2': 2}
216
+ """
217
+ return {q.question_name: i for i, q in enumerate(self.questions)}
218
+
576
219
  def add_question(self, question: QuestionBase) -> Survey:
577
220
  """
578
221
  Add a question to survey.
@@ -590,11 +233,11 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
590
233
  >>> s = Survey().add_question(q).add_question(q)
591
234
  Traceback (most recent call last):
592
235
  ...
593
- edsl.exceptions.surveys.SurveyCreationError: Question name 'q0' already exists in survey. Existing names are ['q0'].
236
+ edsl.exceptions.surveys.SurveyCreationError: Question name already exists in survey. ...
594
237
  """
595
238
  if question.question_name in self.question_names:
596
239
  raise SurveyCreationError(
597
- f"""Question name '{question.question_name}' already exists in survey. Existing names are {self.question_names}."""
240
+ f"""Question name already exists in survey. Please use a different name for the offensing question. The problemetic question name is {question.question_name}."""
598
241
  )
599
242
  index = len(self.questions)
600
243
  # TODO: This is a bit ugly because the user
@@ -602,8 +245,6 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
602
245
  # descriptor.
603
246
  self._questions.append(question)
604
247
 
605
- self.pseudo_indices[question.question_name] = index
606
-
607
248
  # using index + 1 presumes there is a next question
608
249
  self.rule_collection.add_rule(
609
250
  Rule(
@@ -622,20 +263,6 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
622
263
 
623
264
  return self
624
265
 
625
- def recombined_questions_and_instructions(
626
- self,
627
- ) -> list[Union[QuestionBase, "Instruction"]]:
628
- """Return a list of questions and instructions sorted by pseudo index."""
629
- questions_and_instructions = self._questions + list(
630
- self.instruction_names_to_instructions.values()
631
- )
632
- return sorted(
633
- questions_and_instructions, key=lambda x: self.pseudo_indices[x.name]
634
- )
635
-
636
- # endregion
637
-
638
- # region: Memory plan methods
639
266
  def set_full_memory_mode(self) -> Survey:
640
267
  """Add instructions to a survey that the agent should remember all of the answers to the questions in the survey.
641
268
 
@@ -668,75 +295,6 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
668
295
  prior_questions=prior_questions_func(i),
669
296
  )
670
297
 
671
- def add_targeted_memory(
672
- self,
673
- focal_question: Union[QuestionBase, str],
674
- prior_question: Union[QuestionBase, str],
675
- ) -> Survey:
676
- """Add instructions to a survey than when answering focal_question.
677
-
678
- :param focal_question: The question that the agent is answering.
679
- :param prior_question: The question that the agent should remember when answering the focal question.
680
-
681
- Here we add instructions to a survey than when answering q2 they should remember q1:
682
-
683
- >>> s = Survey.example().add_targeted_memory("q2", "q0")
684
- >>> s.memory_plan
685
- {'q2': Memory(prior_questions=['q0'])}
686
-
687
- The agent should also remember the answers to prior_questions listed in prior_questions.
688
- """
689
- focal_question_name = self.question_names[
690
- self._get_question_index(focal_question)
691
- ]
692
- prior_question_name = self.question_names[
693
- self._get_question_index(prior_question)
694
- ]
695
-
696
- self.memory_plan.add_single_memory(
697
- focal_question=focal_question_name,
698
- prior_question=prior_question_name,
699
- )
700
-
701
- return self
702
-
703
- def add_memory_collection(
704
- self,
705
- focal_question: Union[QuestionBase, str],
706
- prior_questions: List[Union[QuestionBase, str]],
707
- ) -> Survey:
708
- """Add prior questions and responses so the agent has them when answering.
709
-
710
- This adds instructions to a survey than when answering focal_question, the agent should also remember the answers to prior_questions listed in prior_questions.
711
-
712
- :param focal_question: The question that the agent is answering.
713
- :param prior_questions: The questions that the agent should remember when answering the focal question.
714
-
715
- Here we have it so that when answering q2, the agent should remember answers to q0 and q1:
716
-
717
- >>> s = Survey.example().add_memory_collection("q2", ["q0", "q1"])
718
- >>> s.memory_plan
719
- {'q2': Memory(prior_questions=['q0', 'q1'])}
720
- """
721
- focal_question_name = self.question_names[
722
- self._get_question_index(focal_question)
723
- ]
724
-
725
- prior_question_names = [
726
- self.question_names[self._get_question_index(prior_question)]
727
- for prior_question in prior_questions
728
- ]
729
-
730
- self.memory_plan.add_memory_collection(
731
- focal_question=focal_question_name, prior_questions=prior_question_names
732
- )
733
- return self
734
-
735
- # endregion
736
- # endregion
737
- # endregion
738
-
739
- # region: Question groups
740
298
  def add_question_group(
741
299
  self,
742
300
  start_question: Union[QuestionBase, str],
@@ -814,24 +372,69 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
814
372
  self.question_groups[group_name] = (start_index, end_index)
815
373
  return self
816
374
 
817
- # endregion
375
+ def add_targeted_memory(
376
+ self,
377
+ focal_question: Union[QuestionBase, str],
378
+ prior_question: Union[QuestionBase, str],
379
+ ) -> Survey:
380
+ """Add instructions to a survey than when answering focal_question.
381
+
382
+ :param focal_question: The question that the agent is answering.
383
+ :param prior_question: The question that the agent should remember when answering the focal question.
384
+
385
+ Here we add instructions to a survey than when answering q2 they should remember q1:
386
+
387
+ >>> s = Survey.example().add_targeted_memory("q2", "q0")
388
+ >>> s.memory_plan
389
+ {'q2': Memory(prior_questions=['q0'])}
390
+
391
+ The agent should also remember the answers to prior_questions listed in prior_questions.
392
+ """
393
+ focal_question_name = self.question_names[
394
+ self._get_question_index(focal_question)
395
+ ]
396
+ prior_question_name = self.question_names[
397
+ self._get_question_index(prior_question)
398
+ ]
399
+
400
+ self.memory_plan.add_single_memory(
401
+ focal_question=focal_question_name,
402
+ prior_question=prior_question_name,
403
+ )
404
+
405
+ return self
406
+
407
+ def add_memory_collection(
408
+ self,
409
+ focal_question: Union[QuestionBase, str],
410
+ prior_questions: List[Union[QuestionBase, str]],
411
+ ) -> Survey:
412
+ """Add prior questions and responses so the agent has them when answering.
818
413
 
819
- # region: Survey rules
820
- def show_rules(self) -> None:
821
- """Print out the rules in the survey.
414
+ This adds instructions to a survey than when answering focal_question, the agent should also remember the answers to prior_questions listed in prior_questions.
822
415
 
823
- >>> s = Survey.example()
824
- >>> s.show_rules()
825
- ┏━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━┓
826
- current_q expression ┃ next_q priority before_rule
827
- ┡━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━┩
828
- 0 │ True │ 1 │ -1 │ False │
829
- 0 │ q0 == 'yes' │ 2 │ 0 │ False │
830
- 1 │ True │ 2 │ -1 │ False │
831
- │ 2 │ True │ 3 │ -1 │ False │
832
- └───────────┴─────────────┴────────┴──────────┴─────────────┘
416
+ :param focal_question: The question that the agent is answering.
417
+ :param prior_questions: The questions that the agent should remember when answering the focal question.
418
+
419
+ Here we have it so that when answering q2, the agent should remember answers to q0 and q1:
420
+
421
+ >>> s = Survey.example().add_memory_collection("q2", ["q0", "q1"])
422
+ >>> s.memory_plan
423
+ {'q2': Memory(prior_questions=['q0', 'q1'])}
833
424
  """
834
- self.rule_collection.show_rules()
425
+ focal_question_name = self.question_names[
426
+ self._get_question_index(focal_question)
427
+ ]
428
+
429
+ prior_question_names = [
430
+ self.question_names[self._get_question_index(prior_question)]
431
+ for prior_question in prior_questions
432
+ ]
433
+
434
+ self.memory_plan.add_memory_collection(
435
+ focal_question=focal_question_name, prior_questions=prior_question_names
436
+ )
437
+ return self
835
438
 
836
439
  def add_stop_rule(
837
440
  self, question: Union[QuestionBase, str], expression: str
@@ -841,7 +444,6 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
841
444
  :param question: The question to add the stop rule to.
842
445
  :param expression: The expression to evaluate.
843
446
 
844
- If this rule is true, the survey ends.
845
447
  The rule is evaluated *after* the question is answered. If the rule is true, the survey ends.
846
448
 
847
449
  Here, answering "yes" to q0 ends the survey:
@@ -854,42 +456,10 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
854
456
 
855
457
  >>> s.next_question("q0", {"q0": "no"}).question_name
856
458
  'q1'
857
-
858
- >>> s.add_stop_rule("q0", "q1 <> 'yes'")
859
- Traceback (most recent call last):
860
- ...
861
- ValueError: The expression contains '<>', which is not allowed. You probably mean '!='.
862
459
  """
863
- expression = ValidatedString(expression)
864
460
  self.add_rule(question, expression, EndOfSurvey)
865
461
  return self
866
462
 
867
- def clear_non_default_rules(self) -> Survey:
868
- """Remove all non-default rules from the survey.
869
-
870
- >>> Survey.example().show_rules()
871
- ┏━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━┓
872
- ┃ current_q ┃ expression ┃ next_q ┃ priority ┃ before_rule ┃
873
- ┡━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━┩
874
- │ 0 │ True │ 1 │ -1 │ False │
875
- │ 0 │ q0 == 'yes' │ 2 │ 0 │ False │
876
- │ 1 │ True │ 2 │ -1 │ False │
877
- │ 2 │ True │ 3 │ -1 │ False │
878
- └───────────┴─────────────┴────────┴──────────┴─────────────┘
879
- >>> Survey.example().clear_non_default_rules().show_rules()
880
- ┏━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━┓
881
- ┃ current_q ┃ expression ┃ next_q ┃ priority ┃ before_rule ┃
882
- ┡━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━┩
883
- │ 0 │ True │ 1 │ -1 │ False │
884
- │ 1 │ True │ 2 │ -1 │ False │
885
- │ 2 │ True │ 3 │ -1 │ False │
886
- └───────────┴────────────┴────────┴──────────┴─────────────┘
887
- """
888
- s = Survey()
889
- for question in self.questions:
890
- s.add_question(question)
891
- return s
892
-
893
463
  def add_skip_rule(
894
464
  self, question: Union[QuestionBase, str], expression: str
895
465
  ) -> Survey:
@@ -917,6 +487,36 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
917
487
  self._add_rule(question, expression, question_index + 1, before_rule=True)
918
488
  return self
919
489
 
490
+ def _get_question_index(
491
+ self, q: Union[QuestionBase, str, EndOfSurvey.__class__]
492
+ ) -> Union[int, EndOfSurvey.__class__]:
493
+ """Return the index of the question or EndOfSurvey object.
494
+
495
+ :param q: The question or question name to get the index of.
496
+
497
+ It can handle it if the user passes in the question name, the question object, or the EndOfSurvey object.
498
+
499
+ >>> s = Survey.example()
500
+ >>> s._get_question_index("q0")
501
+ 0
502
+
503
+ This doesnt' work with questions that don't exist:
504
+
505
+ >>> s._get_question_index("poop")
506
+ Traceback (most recent call last):
507
+ ...
508
+ ValueError: Question name poop not found in survey. The current question names are {'q0': 0, 'q1': 1, 'q2': 2}.
509
+ """
510
+ if q == EndOfSurvey:
511
+ return EndOfSurvey
512
+ else:
513
+ question_name = q if isinstance(q, str) else q.question_name
514
+ if question_name not in self.question_name_to_index:
515
+ raise ValueError(
516
+ f"""Question name {question_name} not found in survey. The current question names are {self.question_name_to_index}."""
517
+ )
518
+ return self.question_name_to_index[question_name]
519
+
920
520
  def _get_new_rule_priority(
921
521
  self, question_index: int, before_rule: bool = False
922
522
  ) -> int:
@@ -1015,9 +615,9 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
1015
615
 
1016
616
  return self
1017
617
 
1018
- # endregion
1019
-
1020
- # region: Forward methods
618
+ ###################
619
+ # FORWARD METHODS
620
+ ###################
1021
621
  def by(self, *args: Union["Agent", "Scenario", "LanguageModel"]) -> "Jobs":
1022
622
  """Add Agents, Scenarios, and LanguageModels to a survey and returns a runnable Jobs object.
1023
623
 
@@ -1040,63 +640,34 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
1040
640
 
1041
641
  return Jobs(survey=self)
1042
642
 
1043
- # endregion
1044
-
1045
- # region: Running the survey
1046
-
1047
- def __call__(self, model=None, agent=None, cache=None, **kwargs):
1048
- """Run the survey with default model, taking the required survey as arguments.
1049
-
1050
- >>> from edsl.questions import QuestionFunctional
1051
- >>> def f(scenario, agent_traits): return "yes" if scenario["period"] == "morning" else "no"
1052
- >>> q = QuestionFunctional(question_name = "q0", func = f)
1053
- >>> s = Survey([q])
1054
- >>> s(period = "morning", cache = False).select("answer.q0").first()
1055
- 'yes'
1056
- >>> s(period = "evening", cache = False).select("answer.q0").first()
1057
- 'no'
1058
- """
1059
- job = self.get_job(model, agent, **kwargs)
1060
- return job.run(cache=cache)
1061
-
1062
- async def run_async(self, model=None, agent=None, cache=None, **kwargs):
1063
- """Run the survey with default model, taking the required survey as arguments.
1064
-
1065
- >>> from edsl.questions import QuestionFunctional
1066
- >>> def f(scenario, agent_traits): return "yes" if scenario["period"] == "morning" else "no"
1067
- >>> q = QuestionFunctional(question_name = "q0", func = f)
1068
- >>> s = Survey([q])
1069
- >>> s(period = "morning").select("answer.q0").first()
1070
- 'yes'
1071
- >>> s(period = "evening").select("answer.q0").first()
1072
- 'no'
1073
- """
1074
- # TODO: temp fix by creating a cache
1075
- if cache is None:
1076
- from edsl.data import Cache
1077
-
1078
- c = Cache()
1079
- else:
1080
- c = cache
1081
- jobs: "Jobs" = self.get_job(model, agent, **kwargs)
1082
- return await jobs.run_async(cache=c)
1083
-
1084
643
  def run(self, *args, **kwargs) -> "Results":
1085
644
  """Turn the survey into a Job and runs it.
1086
645
 
646
+ Here we run a survey but with debug mode on (so LLM calls are not made)
647
+
1087
648
  >>> from edsl import QuestionFreeText
1088
649
  >>> s = Survey([QuestionFreeText.example()])
1089
- >>> from edsl.language_models import LanguageModel
1090
- >>> m = LanguageModel.example(test_model = True, canned_response = "Great!")
1091
- >>> results = s.by(m).run(cache = False)
1092
- >>> results.select('answer.*')
1093
- Dataset([{'answer.how_are_you': ['Great!']}])
650
+ >>> results = s.run(debug = True, cache = False)
651
+ >>> results.select('answer.*').print(format = "rich")
652
+ ┏━━━━━━━━━━━━━━┓
653
+ answer
654
+ .how_are_you
655
+ ┡━━━━━━━━━━━━━━┩
656
+ ...
657
+ └──────────────┘
1094
658
  """
1095
659
  from edsl.jobs.Jobs import Jobs
1096
660
 
1097
661
  return Jobs(survey=self).run(*args, **kwargs)
1098
662
 
1099
- # region: Survey flow
663
+ ########################
664
+ ## Survey-Taking Methods
665
+ ########################
666
+
667
+ def _first_question(self) -> QuestionBase:
668
+ """Return the first question in the survey."""
669
+ return self.questions[0]
670
+
1100
671
  def next_question(
1101
672
  self, current_question: Union[str, QuestionBase], answers: dict
1102
673
  ) -> Union[QuestionBase, EndOfSurvey.__class__]:
@@ -1174,15 +745,9 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
1174
745
  Question('multiple_choice', question_name = \"""q0\""", question_text = \"""Do you like school?\""", question_options = ['yes', 'no'])
1175
746
  >>> i2.send({"q0": "no"})
1176
747
  Question('multiple_choice', question_name = \"""q1\""", question_text = \"""Why not?\""", question_options = ['killer bees in cafeteria', 'other'])
1177
-
1178
-
1179
748
  """
1180
749
  self.answers = {}
1181
- question = self._questions[0]
1182
- # should the first question be skipped?
1183
- if self.rule_collection.skip_question_before_running(0, self.answers):
1184
- question = self.next_question(question, self.answers)
1185
-
750
+ question = self._first_question()
1186
751
  while not question == EndOfSurvey:
1187
752
  # breakpoint()
1188
753
  answer = yield question
@@ -1191,9 +756,34 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
1191
756
  ## TODO: This should also include survey and agent attributes
1192
757
  question = self.next_question(question, self.answers)
1193
758
 
1194
- # endregion
759
+ @property
760
+ def scenario_attributes(self) -> list[str]:
761
+ """Return a list of attributes that admissible Scenarios should have.
762
+
763
+ Here we have a survey with a question that uses a jinja2 style {{ }} template:
764
+
765
+ >>> from edsl import QuestionFreeText
766
+ >>> s = Survey().add_question(QuestionFreeText(question_text="{{ greeting }}. What is your name?", question_name="name"))
767
+ >>> s.scenario_attributes
768
+ ['greeting']
769
+
770
+ >>> s = Survey().add_question(QuestionFreeText(question_text="{{ greeting }}. What is your {{ attribute }}?", question_name="name"))
771
+ >>> s.scenario_attributes
772
+ ['greeting', 'attribute']
773
+
774
+
775
+ """
776
+ temp = []
777
+ for question in self.questions:
778
+ question_text = question.question_text
779
+ # extract the contents of all {{ }} in the question text using regex
780
+ matches = re.findall(r"\{\{(.+?)\}\}", question_text)
781
+ # remove whitespace
782
+ matches = [match.strip() for match in matches]
783
+ # add them to the temp list
784
+ temp.extend(matches)
785
+ return temp
1195
786
 
1196
- # regions: DAG construction
1197
787
  def textify(self, index_dag: DAG) -> DAG:
1198
788
  """Convert the DAG of question indices to a DAG of question names.
1199
789
 
@@ -1333,6 +923,59 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
1333
923
  return False
1334
924
  return self.to_dict() == other.to_dict()
1335
925
 
926
+ ###################
927
+ # SERIALIZATION METHODS
928
+ ###################
929
+
930
+ def _to_dict(self) -> dict[str, Any]:
931
+ """Serialize the Survey object to a dictionary.
932
+
933
+ >>> s = Survey.example()
934
+ >>> s._to_dict().keys()
935
+ dict_keys(['questions', 'memory_plan', 'rule_collection', 'question_groups'])
936
+
937
+ """
938
+ return {
939
+ "questions": [q._to_dict() for q in self._questions],
940
+ "memory_plan": self.memory_plan.to_dict(),
941
+ "rule_collection": self.rule_collection.to_dict(),
942
+ "question_groups": self.question_groups,
943
+ }
944
+
945
+ @add_edsl_version
946
+ def to_dict(self) -> dict[str, Any]:
947
+ """Serialize the Survey object to a dictionary.
948
+
949
+ >>> s = Survey.example()
950
+ >>> s.to_dict().keys()
951
+ dict_keys(['questions', 'memory_plan', 'rule_collection', 'question_groups', 'edsl_version', 'edsl_class_name'])
952
+
953
+ """
954
+ return self._to_dict()
955
+
956
+ @classmethod
957
+ @remove_edsl_version
958
+ def from_dict(cls, data: dict) -> Survey:
959
+ """Deserialize the dictionary back to a Survey object.
960
+
961
+ :param data: The dictionary to deserialize.
962
+
963
+ >>> d = Survey.example().to_dict()
964
+ >>> s = Survey.from_dict(d)
965
+ >>> s == Survey.example()
966
+ True
967
+
968
+ """
969
+ questions = [QuestionBase.from_dict(q_dict) for q_dict in data["questions"]]
970
+ memory_plan = MemoryPlan.from_dict(data["memory_plan"])
971
+ survey = cls(
972
+ questions=questions,
973
+ memory_plan=memory_plan,
974
+ rule_collection=RuleCollection.from_dict(data["rule_collection"]),
975
+ question_groups=data["question_groups"],
976
+ )
977
+ return survey
978
+
1336
979
  @classmethod
1337
980
  def from_qsf(
1338
981
  cls, qsf_file: Optional[str] = None, url: Optional[str] = None
@@ -1359,7 +1002,9 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
1359
1002
  so = SurveyQualtricsImport(qsf_file)
1360
1003
  return so.create_survey()
1361
1004
 
1362
- # region: Display methods
1005
+ ###################
1006
+ # DISPLAY METHODS
1007
+ ###################
1363
1008
  def print(self):
1364
1009
  """Print the survey in a rich format.
1365
1010
 
@@ -1378,8 +1023,7 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
1378
1023
  def __repr__(self) -> str:
1379
1024
  """Return a string representation of the survey."""
1380
1025
 
1381
- # questions_string = ", ".join([repr(q) for q in self._questions])
1382
- questions_string = ", ".join([repr(q) for q in self.raw_passed_questions or []])
1026
+ questions_string = ", ".join([repr(q) for q in self._questions])
1383
1027
  # question_names_string = ", ".join([repr(name) for name in self.question_names])
1384
1028
  return f"Survey(questions=[{questions_string}], memory_plan={self.memory_plan}, rule_collection={self.rule_collection}, question_groups={self.question_groups})"
1385
1029
 
@@ -1388,6 +1032,22 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
1388
1032
 
1389
1033
  return data_to_html(self.to_dict())
1390
1034
 
1035
+ def show_rules(self) -> None:
1036
+ """Print out the rules in the survey.
1037
+
1038
+ >>> s = Survey.example()
1039
+ >>> s.show_rules()
1040
+ ┏━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━┓
1041
+ ┃ current_q ┃ expression ┃ next_q ┃ priority ┃ before_rule ┃
1042
+ ┡━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━┩
1043
+ │ 0 │ True │ 1 │ -1 │ False │
1044
+ │ 0 │ q0 == 'yes' │ 2 │ 0 │ False │
1045
+ │ 1 │ True │ 2 │ -1 │ False │
1046
+ │ 2 │ True │ 3 │ -1 │ False │
1047
+ └───────────┴─────────────┴────────┴──────────┴─────────────┘
1048
+ """
1049
+ self.rule_collection.show_rules()
1050
+
1391
1051
  def rich_print(self) -> Table:
1392
1052
  """Print the survey in a rich format.
1393
1053
 
@@ -1423,8 +1083,6 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
1423
1083
 
1424
1084
  return table
1425
1085
 
1426
- # endregion
1427
-
1428
1086
  def codebook(self) -> dict[str, str]:
1429
1087
  """Create a codebook for the survey, mapping question names to question text.
1430
1088
 
@@ -1437,7 +1095,6 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
1437
1095
  codebook[question.question_name] = question.question_text
1438
1096
  return codebook
1439
1097
 
1440
- # region: Export methods
1441
1098
  def to_csv(self, filename: str = None):
1442
1099
  """Export the survey to a CSV file.
1443
1100
 
@@ -1480,16 +1137,8 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
1480
1137
  res = c.web(self.to_dict(), platform, email)
1481
1138
  return res
1482
1139
 
1483
- # endregion
1484
-
1485
1140
  @classmethod
1486
- def example(
1487
- cls,
1488
- params: bool = False,
1489
- randomize: bool = False,
1490
- include_instructions=False,
1491
- custom_instructions: Optional[str] = None,
1492
- ) -> Survey:
1141
+ def example(cls, params: bool = False, randomize: bool = False) -> Survey:
1493
1142
  """Return an example survey.
1494
1143
 
1495
1144
  >>> s = Survey.example()
@@ -1522,18 +1171,6 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
1522
1171
  )
1523
1172
  s = cls(questions=[q0, q1, q2, q3])
1524
1173
  return s
1525
-
1526
- if include_instructions:
1527
- from edsl import Instruction
1528
-
1529
- custom_instructions = (
1530
- custom_instructions if custom_instructions else "Please pay attention!"
1531
- )
1532
-
1533
- i = Instruction(text=custom_instructions, name="attention")
1534
- s = cls(questions=[i, q0, q1, q2])
1535
- return s
1536
-
1537
1174
  s = cls(questions=[q0, q1, q2])
1538
1175
  s = s.add_rule(q0, "q0 == 'yes'", q2)
1539
1176
  return s
@@ -1555,6 +1192,43 @@ class Survey(SurveyExportMixin, SurveyFlowVisualizationMixin, Base):
1555
1192
 
1556
1193
  return self.by(s).by(agent).by(model)
1557
1194
 
1195
+ def __call__(self, model=None, agent=None, cache=None, **kwargs):
1196
+ """Run the survey with default model, taking the required survey as arguments.
1197
+
1198
+ >>> from edsl.questions import QuestionFunctional
1199
+ >>> def f(scenario, agent_traits): return "yes" if scenario["period"] == "morning" else "no"
1200
+ >>> q = QuestionFunctional(question_name = "q0", func = f)
1201
+ >>> s = Survey([q])
1202
+ >>> s(period = "morning", cache = False).select("answer.q0").first()
1203
+ 'yes'
1204
+ >>> s(period = "evening", cache = False).select("answer.q0").first()
1205
+ 'no'
1206
+ """
1207
+ job = self.get_job(model, agent, **kwargs)
1208
+ return job.run(cache=cache)
1209
+
1210
+ async def run_async(self, model=None, agent=None, cache=None, **kwargs):
1211
+ """Run the survey with default model, taking the required survey as arguments.
1212
+
1213
+ >>> from edsl.questions import QuestionFunctional
1214
+ >>> def f(scenario, agent_traits): return "yes" if scenario["period"] == "morning" else "no"
1215
+ >>> q = QuestionFunctional(question_name = "q0", func = f)
1216
+ >>> s = Survey([q])
1217
+ >>> s(period = "morning").select("answer.q0").first()
1218
+ 'yes'
1219
+ >>> s(period = "evening").select("answer.q0").first()
1220
+ 'no'
1221
+ """
1222
+ # TODO: temp fix by creating a cache
1223
+ if cache is None:
1224
+ from edsl.data import Cache
1225
+
1226
+ c = Cache()
1227
+ else:
1228
+ c = cache
1229
+ jobs: "Jobs" = self.get_job(model, agent, **kwargs)
1230
+ return await jobs.run_async(cache=c)
1231
+
1558
1232
 
1559
1233
  def main():
1560
1234
  """Run the example survey."""