edsl 0.1.39.dev1__py3-none-any.whl → 0.1.39.dev2__py3-none-any.whl

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