edsl 0.1.38.dev4__py3-none-any.whl → 0.1.39__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. edsl/Base.py +197 -116
  2. edsl/__init__.py +15 -7
  3. edsl/__version__.py +1 -1
  4. edsl/agents/Agent.py +351 -147
  5. edsl/agents/AgentList.py +211 -73
  6. edsl/agents/Invigilator.py +101 -50
  7. edsl/agents/InvigilatorBase.py +62 -70
  8. edsl/agents/PromptConstructor.py +143 -225
  9. edsl/agents/QuestionInstructionPromptBuilder.py +128 -0
  10. edsl/agents/QuestionTemplateReplacementsBuilder.py +137 -0
  11. edsl/agents/__init__.py +0 -1
  12. edsl/agents/prompt_helpers.py +3 -3
  13. edsl/agents/question_option_processor.py +172 -0
  14. edsl/auto/AutoStudy.py +18 -5
  15. edsl/auto/StageBase.py +53 -40
  16. edsl/auto/StageQuestions.py +2 -1
  17. edsl/auto/utilities.py +0 -6
  18. edsl/config.py +22 -2
  19. edsl/conversation/car_buying.py +2 -1
  20. edsl/coop/CoopFunctionsMixin.py +15 -0
  21. edsl/coop/ExpectedParrotKeyHandler.py +125 -0
  22. edsl/coop/PriceFetcher.py +1 -1
  23. edsl/coop/coop.py +125 -47
  24. edsl/coop/utils.py +14 -14
  25. edsl/data/Cache.py +45 -27
  26. edsl/data/CacheEntry.py +12 -15
  27. edsl/data/CacheHandler.py +31 -12
  28. edsl/data/RemoteCacheSync.py +154 -46
  29. edsl/data/__init__.py +4 -3
  30. edsl/data_transfer_models.py +2 -1
  31. edsl/enums.py +27 -0
  32. edsl/exceptions/__init__.py +50 -50
  33. edsl/exceptions/agents.py +12 -0
  34. edsl/exceptions/inference_services.py +5 -0
  35. edsl/exceptions/questions.py +24 -6
  36. edsl/exceptions/scenarios.py +7 -0
  37. edsl/inference_services/AnthropicService.py +38 -19
  38. edsl/inference_services/AvailableModelCacheHandler.py +184 -0
  39. edsl/inference_services/AvailableModelFetcher.py +215 -0
  40. edsl/inference_services/AwsBedrock.py +0 -2
  41. edsl/inference_services/AzureAI.py +0 -2
  42. edsl/inference_services/GoogleService.py +7 -12
  43. edsl/inference_services/InferenceServiceABC.py +18 -85
  44. edsl/inference_services/InferenceServicesCollection.py +120 -79
  45. edsl/inference_services/MistralAIService.py +0 -3
  46. edsl/inference_services/OpenAIService.py +47 -35
  47. edsl/inference_services/PerplexityService.py +0 -3
  48. edsl/inference_services/ServiceAvailability.py +135 -0
  49. edsl/inference_services/TestService.py +11 -10
  50. edsl/inference_services/TogetherAIService.py +5 -3
  51. edsl/inference_services/data_structures.py +134 -0
  52. edsl/jobs/AnswerQuestionFunctionConstructor.py +223 -0
  53. edsl/jobs/Answers.py +1 -14
  54. edsl/jobs/FetchInvigilator.py +47 -0
  55. edsl/jobs/InterviewTaskManager.py +98 -0
  56. edsl/jobs/InterviewsConstructor.py +50 -0
  57. edsl/jobs/Jobs.py +356 -431
  58. edsl/jobs/JobsChecks.py +35 -10
  59. edsl/jobs/JobsComponentConstructor.py +189 -0
  60. edsl/jobs/JobsPrompts.py +6 -4
  61. edsl/jobs/JobsRemoteInferenceHandler.py +205 -133
  62. edsl/jobs/JobsRemoteInferenceLogger.py +239 -0
  63. edsl/jobs/RequestTokenEstimator.py +30 -0
  64. edsl/jobs/async_interview_runner.py +138 -0
  65. edsl/jobs/buckets/BucketCollection.py +44 -3
  66. edsl/jobs/buckets/TokenBucket.py +53 -21
  67. edsl/jobs/buckets/TokenBucketAPI.py +211 -0
  68. edsl/jobs/buckets/TokenBucketClient.py +191 -0
  69. edsl/jobs/check_survey_scenario_compatibility.py +85 -0
  70. edsl/jobs/data_structures.py +120 -0
  71. edsl/jobs/decorators.py +35 -0
  72. edsl/jobs/interviews/Interview.py +143 -408
  73. edsl/jobs/jobs_status_enums.py +9 -0
  74. edsl/jobs/loggers/HTMLTableJobLogger.py +304 -0
  75. edsl/jobs/results_exceptions_handler.py +98 -0
  76. edsl/jobs/runners/JobsRunnerAsyncio.py +88 -403
  77. edsl/jobs/runners/JobsRunnerStatus.py +133 -165
  78. edsl/jobs/tasks/QuestionTaskCreator.py +21 -19
  79. edsl/jobs/tasks/TaskHistory.py +38 -18
  80. edsl/jobs/tasks/task_status_enum.py +0 -2
  81. edsl/language_models/ComputeCost.py +63 -0
  82. edsl/language_models/LanguageModel.py +194 -236
  83. edsl/language_models/ModelList.py +28 -19
  84. edsl/language_models/PriceManager.py +127 -0
  85. edsl/language_models/RawResponseHandler.py +106 -0
  86. edsl/language_models/ServiceDataSources.py +0 -0
  87. edsl/language_models/__init__.py +1 -2
  88. edsl/language_models/key_management/KeyLookup.py +63 -0
  89. edsl/language_models/key_management/KeyLookupBuilder.py +273 -0
  90. edsl/language_models/key_management/KeyLookupCollection.py +38 -0
  91. edsl/language_models/key_management/__init__.py +0 -0
  92. edsl/language_models/key_management/models.py +131 -0
  93. edsl/language_models/model.py +256 -0
  94. edsl/language_models/repair.py +2 -2
  95. edsl/language_models/utilities.py +5 -4
  96. edsl/notebooks/Notebook.py +19 -14
  97. edsl/notebooks/NotebookToLaTeX.py +142 -0
  98. edsl/prompts/Prompt.py +29 -39
  99. edsl/questions/ExceptionExplainer.py +77 -0
  100. edsl/questions/HTMLQuestion.py +103 -0
  101. edsl/questions/QuestionBase.py +68 -214
  102. edsl/questions/QuestionBasePromptsMixin.py +7 -3
  103. edsl/questions/QuestionBudget.py +1 -1
  104. edsl/questions/QuestionCheckBox.py +3 -3
  105. edsl/questions/QuestionExtract.py +5 -7
  106. edsl/questions/QuestionFreeText.py +2 -3
  107. edsl/questions/QuestionList.py +10 -18
  108. edsl/questions/QuestionMatrix.py +265 -0
  109. edsl/questions/QuestionMultipleChoice.py +67 -23
  110. edsl/questions/QuestionNumerical.py +2 -4
  111. edsl/questions/QuestionRank.py +7 -17
  112. edsl/questions/SimpleAskMixin.py +4 -3
  113. edsl/questions/__init__.py +2 -1
  114. edsl/questions/{AnswerValidatorMixin.py → answer_validator_mixin.py} +47 -2
  115. edsl/questions/data_structures.py +20 -0
  116. edsl/questions/derived/QuestionLinearScale.py +6 -3
  117. edsl/questions/derived/QuestionTopK.py +1 -1
  118. edsl/questions/descriptors.py +17 -3
  119. edsl/questions/loop_processor.py +149 -0
  120. edsl/questions/{QuestionBaseGenMixin.py → question_base_gen_mixin.py} +57 -50
  121. edsl/questions/question_registry.py +1 -1
  122. edsl/questions/{ResponseValidatorABC.py → response_validator_abc.py} +40 -26
  123. edsl/questions/response_validator_factory.py +34 -0
  124. edsl/questions/templates/matrix/__init__.py +1 -0
  125. edsl/questions/templates/matrix/answering_instructions.jinja +5 -0
  126. edsl/questions/templates/matrix/question_presentation.jinja +20 -0
  127. edsl/results/CSSParameterizer.py +1 -1
  128. edsl/results/Dataset.py +170 -7
  129. edsl/results/DatasetExportMixin.py +168 -305
  130. edsl/results/DatasetTree.py +28 -8
  131. edsl/results/MarkdownToDocx.py +122 -0
  132. edsl/results/MarkdownToPDF.py +111 -0
  133. edsl/results/Result.py +298 -206
  134. edsl/results/Results.py +149 -131
  135. edsl/results/ResultsExportMixin.py +2 -0
  136. edsl/results/TableDisplay.py +98 -171
  137. edsl/results/TextEditor.py +50 -0
  138. edsl/results/__init__.py +1 -1
  139. edsl/results/file_exports.py +252 -0
  140. edsl/results/{Selector.py → results_selector.py} +23 -13
  141. edsl/results/smart_objects.py +96 -0
  142. edsl/results/table_data_class.py +12 -0
  143. edsl/results/table_renderers.py +118 -0
  144. edsl/scenarios/ConstructDownloadLink.py +109 -0
  145. edsl/scenarios/DocumentChunker.py +102 -0
  146. edsl/scenarios/DocxScenario.py +16 -0
  147. edsl/scenarios/FileStore.py +150 -239
  148. edsl/scenarios/PdfExtractor.py +40 -0
  149. edsl/scenarios/Scenario.py +90 -193
  150. edsl/scenarios/ScenarioHtmlMixin.py +4 -3
  151. edsl/scenarios/ScenarioList.py +415 -244
  152. edsl/scenarios/ScenarioListExportMixin.py +0 -7
  153. edsl/scenarios/ScenarioListPdfMixin.py +15 -37
  154. edsl/scenarios/__init__.py +1 -2
  155. edsl/scenarios/directory_scanner.py +96 -0
  156. edsl/scenarios/file_methods.py +85 -0
  157. edsl/scenarios/handlers/__init__.py +13 -0
  158. edsl/scenarios/handlers/csv.py +49 -0
  159. edsl/scenarios/handlers/docx.py +76 -0
  160. edsl/scenarios/handlers/html.py +37 -0
  161. edsl/scenarios/handlers/json.py +111 -0
  162. edsl/scenarios/handlers/latex.py +5 -0
  163. edsl/scenarios/handlers/md.py +51 -0
  164. edsl/scenarios/handlers/pdf.py +68 -0
  165. edsl/scenarios/handlers/png.py +39 -0
  166. edsl/scenarios/handlers/pptx.py +105 -0
  167. edsl/scenarios/handlers/py.py +294 -0
  168. edsl/scenarios/handlers/sql.py +313 -0
  169. edsl/scenarios/handlers/sqlite.py +149 -0
  170. edsl/scenarios/handlers/txt.py +33 -0
  171. edsl/scenarios/{ScenarioJoin.py → scenario_join.py} +10 -6
  172. edsl/scenarios/scenario_selector.py +156 -0
  173. edsl/study/ObjectEntry.py +1 -1
  174. edsl/study/SnapShot.py +1 -1
  175. edsl/study/Study.py +5 -12
  176. edsl/surveys/ConstructDAG.py +92 -0
  177. edsl/surveys/EditSurvey.py +221 -0
  178. edsl/surveys/InstructionHandler.py +100 -0
  179. edsl/surveys/MemoryManagement.py +72 -0
  180. edsl/surveys/Rule.py +5 -4
  181. edsl/surveys/RuleCollection.py +25 -27
  182. edsl/surveys/RuleManager.py +172 -0
  183. edsl/surveys/Simulator.py +75 -0
  184. edsl/surveys/Survey.py +270 -791
  185. edsl/surveys/SurveyCSS.py +20 -8
  186. edsl/surveys/{SurveyFlowVisualizationMixin.py → SurveyFlowVisualization.py} +11 -9
  187. edsl/surveys/SurveyToApp.py +141 -0
  188. edsl/surveys/__init__.py +4 -2
  189. edsl/surveys/descriptors.py +6 -2
  190. edsl/surveys/instructions/ChangeInstruction.py +1 -2
  191. edsl/surveys/instructions/Instruction.py +4 -13
  192. edsl/surveys/instructions/InstructionCollection.py +11 -6
  193. edsl/templates/error_reporting/interview_details.html +1 -1
  194. edsl/templates/error_reporting/report.html +1 -1
  195. edsl/tools/plotting.py +1 -1
  196. edsl/utilities/PrettyList.py +56 -0
  197. edsl/utilities/is_notebook.py +18 -0
  198. edsl/utilities/is_valid_variable_name.py +11 -0
  199. edsl/utilities/remove_edsl_version.py +24 -0
  200. edsl/utilities/utilities.py +35 -23
  201. {edsl-0.1.38.dev4.dist-info → edsl-0.1.39.dist-info}/METADATA +12 -10
  202. edsl-0.1.39.dist-info/RECORD +358 -0
  203. {edsl-0.1.38.dev4.dist-info → edsl-0.1.39.dist-info}/WHEEL +1 -1
  204. edsl/language_models/KeyLookup.py +0 -30
  205. edsl/language_models/registry.py +0 -190
  206. edsl/language_models/unused/ReplicateBase.py +0 -83
  207. edsl/results/ResultsDBMixin.py +0 -238
  208. edsl-0.1.38.dev4.dist-info/RECORD +0 -277
  209. /edsl/questions/{RegisterQuestionsMeta.py → register_questions_meta.py} +0 -0
  210. /edsl/results/{ResultsFetchMixin.py → results_fetch_mixin.py} +0 -0
  211. /edsl/results/{ResultsToolsMixin.py → results_tools_mixin.py} +0 -0
  212. {edsl-0.1.38.dev4.dist-info → edsl-0.1.39.dist-info}/LICENSE +0 -0
edsl/study/ObjectEntry.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import time
2
2
  import webbrowser
3
3
  from typing import Any, Dict, Optional, Type
4
- from edsl import QuestionBase
4
+ from edsl.questions.QuestionBase import QuestionBase
5
5
  from edsl.Base import RegisterSubclassesMeta
6
6
 
7
7
 
edsl/study/SnapShot.py CHANGED
@@ -32,7 +32,7 @@ class SnapShot:
32
32
  {'Cache': <class 'edsl.data.Cache.Cache'>}
33
33
  """
34
34
  from edsl.Base import RegisterSubclassesMeta
35
- from edsl import QuestionBase
35
+ from edsl.questions.QuestionBase import QuestionBase
36
36
 
37
37
  all_edsl_objects = RegisterSubclassesMeta.get_registry()
38
38
 
edsl/study/Study.py CHANGED
@@ -7,7 +7,9 @@ import socket
7
7
  from datetime import datetime
8
8
  from typing import Dict, Optional, Union
9
9
  from uuid import UUID, uuid4
10
- from edsl import Cache, set_session_cache, unset_session_cache
10
+
11
+ from edsl.data.Cache import Cache
12
+ from edsl import set_session_cache, unset_session_cache
11
13
  from edsl.utilities.utilities import dict_hash
12
14
  from edsl.study.ObjectEntry import ObjectEntry
13
15
  from edsl.study.ProofOfWork import ProofOfWork
@@ -405,7 +407,7 @@ class Study:
405
407
 
406
408
  study_file = tempfile.NamedTemporaryFile()
407
409
  with cls(filename=study_file.name, verbose=verbose) as study:
408
- from edsl import QuestionFreeText
410
+ from edsl.questions.QuestionFreeText import QuestionFreeText
409
411
 
410
412
  q = QuestionFreeText.example(randomize=randomize)
411
413
  return study
@@ -464,7 +466,7 @@ class Study:
464
466
  def push(self) -> dict:
465
467
  """Push the objects to coop."""
466
468
 
467
- from edsl import Coop
469
+ from edsl.coop.coop import Coop
468
470
 
469
471
  coop = Coop()
470
472
  return coop.create(self, description=self.description)
@@ -517,12 +519,3 @@ if __name__ == "__main__":
517
519
  import doctest
518
520
 
519
521
  doctest.testmod(optionflags=doctest.ELLIPSIS)
520
-
521
- # with Study(name = "cool_study") as study:
522
- # from edsl import QuestionFreeText
523
- # q = QuestionFreeText.example()
524
-
525
- # assert len(study.objects) == 1
526
-
527
- # print(study.versions())
528
- # {'q': [ObjectEntry(variable_name='q', object=Question('free_text', question_name = """how_are_you""", question_text = """How are you?"""), description='Question name: how_are_you', coop_info=None, created_at=1720276402.561273, edsl_class_name='QuestionFreeText')]}
@@ -0,0 +1,92 @@
1
+ from edsl.surveys.base import EndOfSurvey
2
+ from edsl.surveys.DAG import DAG
3
+ from edsl.exceptions.surveys import SurveyError
4
+
5
+
6
+ class ConstructDAG:
7
+ def __init__(self, survey):
8
+ self.survey = survey
9
+ self.questions = survey.questions
10
+
11
+ self.parameters_by_question = self.survey.parameters_by_question
12
+ self.question_name_to_index = self.survey.question_name_to_index
13
+
14
+ def dag(self, textify: bool = False) -> DAG:
15
+ memory_dag = self.survey.memory_plan.dag
16
+ rule_dag = self.survey.rule_collection.dag
17
+ piping_dag = self.piping_dag
18
+ if textify:
19
+ memory_dag = DAG(self.textify(memory_dag))
20
+ rule_dag = DAG(self.textify(rule_dag))
21
+ piping_dag = DAG(self.textify(piping_dag))
22
+ return memory_dag + rule_dag + piping_dag
23
+
24
+ @property
25
+ def piping_dag(self) -> DAG:
26
+ """Figures out the DAG of piping dependencies.
27
+
28
+ >>> from edsl import Survey
29
+ >>> from edsl import QuestionFreeText
30
+ >>> q0 = QuestionFreeText(question_text="Here is a question", question_name="q0")
31
+ >>> q1 = QuestionFreeText(question_text="You previously answered {{ q0 }}---how do you feel now?", question_name="q1")
32
+ >>> s = Survey([q0, q1])
33
+ >>> ConstructDAG(s).piping_dag
34
+ {1: {0}}
35
+ """
36
+ d = {}
37
+ for question_name, depenencies in self.parameters_by_question.items():
38
+ if depenencies:
39
+ question_index = self.question_name_to_index[question_name]
40
+ for dependency in depenencies:
41
+ if dependency not in self.question_name_to_index:
42
+ pass
43
+ else:
44
+ dependency_index = self.question_name_to_index[dependency]
45
+ if question_index not in d:
46
+ d[question_index] = set()
47
+ d[question_index].add(dependency_index)
48
+ return d
49
+
50
+ def textify(self, index_dag: DAG) -> DAG:
51
+ """Convert the DAG of question indices to a DAG of question names.
52
+
53
+ :param index_dag: The DAG of question indices.
54
+
55
+ Example:
56
+
57
+ >>> from edsl import Survey
58
+ >>> s = Survey.example()
59
+ >>> d = s.dag()
60
+ >>> d
61
+ {1: {0}, 2: {0}}
62
+ >>> ConstructDAG(s).textify(d)
63
+ {'q1': {'q0'}, 'q2': {'q0'}}
64
+ """
65
+
66
+ def get_name(index: int):
67
+ """Return the name of the question given the index."""
68
+ if index >= len(self.questions):
69
+ return EndOfSurvey
70
+ try:
71
+ return self.questions[index].question_name
72
+ except IndexError:
73
+ print(
74
+ f"The index is {index} but the length of the questions is {len(self.questions)}"
75
+ )
76
+ raise SurveyError
77
+
78
+ try:
79
+ text_dag = {}
80
+ for child_index, parent_indices in index_dag.items():
81
+ parent_names = {get_name(index) for index in parent_indices}
82
+ child_name = get_name(child_index)
83
+ text_dag[child_name] = parent_names
84
+ return text_dag
85
+ except IndexError:
86
+ raise
87
+
88
+
89
+ if __name__ == "__main__":
90
+ import doctest
91
+
92
+ doctest.testmod()
@@ -0,0 +1,221 @@
1
+ from typing import Union, Optional, TYPE_CHECKING
2
+ from edsl.exceptions.surveys import SurveyError
3
+
4
+ if TYPE_CHECKING:
5
+ from edsl.questions.QuestionBase import QuestionBase
6
+
7
+ from edsl.exceptions.surveys import SurveyError, SurveyCreationError
8
+ from edsl.surveys.Rule import Rule
9
+ from edsl.surveys.base import RulePriority, EndOfSurvey
10
+
11
+
12
+ class EditSurvey:
13
+ def __init__(self, survey):
14
+ self.survey = survey
15
+
16
+ def move_question(self, identifier: Union[str, int], new_index: int) -> "Survey":
17
+ if isinstance(identifier, str):
18
+ if identifier not in self.survey.question_names:
19
+ raise SurveyError(
20
+ f"Question name '{identifier}' does not exist in the survey."
21
+ )
22
+ index = self.survey.question_name_to_index[identifier]
23
+ elif isinstance(identifier, int):
24
+ if identifier < 0 or identifier >= len(self.survey.questions):
25
+ raise SurveyError(f"Index {identifier} is out of range.")
26
+ index = identifier
27
+ else:
28
+ raise SurveyError(
29
+ "Identifier must be either a string (question name) or an integer (question index)."
30
+ )
31
+
32
+ moving_question = self.survey._questions[index]
33
+
34
+ new_survey = self.survey.delete_question(index)
35
+ new_survey.add_question(moving_question, new_index)
36
+ return new_survey
37
+
38
+ def add_question(
39
+ self, question: "QuestionBase", index: Optional[int] = None
40
+ ) -> "Survey":
41
+ if question.question_name in self.survey.question_names:
42
+ raise SurveyCreationError(
43
+ f"""Question name '{question.question_name}' already exists in survey. Existing names are {self.survey.question_names}."""
44
+ )
45
+ if index is None:
46
+ index = len(self.survey.questions)
47
+
48
+ if index > len(self.survey.questions):
49
+ raise SurveyCreationError(
50
+ f"Index {index} is greater than the number of questions in the survey."
51
+ )
52
+ if index < 0:
53
+ raise SurveyCreationError(f"Index {index} is less than 0.")
54
+
55
+ interior_insertion = index != len(self.survey.questions)
56
+
57
+ # index = len(self.survey.questions)
58
+ # TODO: This is a bit ugly because the user
59
+ # doesn't "know" about _questions - it's generated by the
60
+ # descriptor.
61
+ self.survey._questions.insert(index, question)
62
+
63
+ if interior_insertion:
64
+ for question_name, old_index in self.survey._pseudo_indices.items():
65
+ if old_index >= index:
66
+ self.survey._pseudo_indices[question_name] = old_index + 1
67
+
68
+ self.survey._pseudo_indices[question.question_name] = index
69
+
70
+ ## Re-do question_name to index - this is done automatically
71
+ # for question_name, old_index in self.survey.question_name_to_index.items():
72
+ # if old_index >= index:
73
+ # self.survey.question_name_to_index[question_name] = old_index + 1
74
+
75
+ ## Need to re-do the rule collection and the indices of the questions
76
+
77
+ ## If a rule is before the insertion index and next_q is also before the insertion index, no change needed.
78
+ ## If the rule is before the insertion index but next_q is after the insertion index, increment the next_q by 1
79
+ ## If the rule is after the insertion index, increment the current_q by 1 and the next_q by 1
80
+
81
+ # using index + 1 presumes there is a next question
82
+ if interior_insertion:
83
+ for rule in self.survey.rule_collection:
84
+ if rule.current_q >= index:
85
+ rule.current_q += 1
86
+ if rule.next_q >= index:
87
+ rule.next_q += 1
88
+
89
+ # add a new rule
90
+ self.survey.rule_collection.add_rule(
91
+ Rule(
92
+ current_q=index,
93
+ expression="True",
94
+ next_q=index + 1,
95
+ question_name_to_index=self.survey.question_name_to_index,
96
+ priority=RulePriority.DEFAULT.value,
97
+ )
98
+ )
99
+
100
+ # a question might be added before the memory plan is created
101
+ # it's ok because the memory plan will be updated when it is created
102
+ if hasattr(self.survey, "memory_plan"):
103
+ self.survey.memory_plan.add_question(question)
104
+
105
+ return self.survey
106
+
107
+ def delete_question(self, identifier: Union[str, int]) -> "Survey":
108
+ """
109
+ Delete a question from the survey.
110
+
111
+ :param identifier: The name or index of the question to delete.
112
+ :return: The updated Survey object.
113
+
114
+ >>> from edsl import QuestionMultipleChoice, Survey
115
+ >>> q1 = QuestionMultipleChoice(question_text="Q1", question_options=["A", "B"], question_name="q1")
116
+ >>> q2 = QuestionMultipleChoice(question_text="Q2", question_options=["C", "D"], question_name="q2")
117
+ >>> s = Survey().add_question(q1).add_question(q2)
118
+ >>> _ = s.delete_question("q1")
119
+ >>> len(s.questions)
120
+ 1
121
+ >>> _ = s.delete_question(0)
122
+ >>> len(s.questions)
123
+ 0
124
+ """
125
+ if isinstance(identifier, str):
126
+ if identifier not in self.survey.question_names:
127
+ raise SurveyError(
128
+ f"Question name '{identifier}' does not exist in the survey."
129
+ )
130
+ index = self.survey.question_name_to_index[identifier]
131
+ elif isinstance(identifier, int):
132
+ if identifier < 0 or identifier >= len(self.survey.questions):
133
+ raise SurveyError(f"Index {identifier} is out of range.")
134
+ index = identifier
135
+ else:
136
+ raise SurveyError(
137
+ "Identifier must be either a string (question name) or an integer (question index)."
138
+ )
139
+
140
+ # Remove the question
141
+ deleted_question = self.survey._questions.pop(index)
142
+ del self.survey._pseudo_indices[deleted_question.question_name]
143
+
144
+ # Update indices
145
+ for question_name, old_index in self.survey._pseudo_indices.items():
146
+ if old_index > index:
147
+ self.survey._pseudo_indices[question_name] = old_index - 1
148
+
149
+ # Update rules
150
+ from .RuleCollection import RuleCollection
151
+
152
+ new_rule_collection = RuleCollection()
153
+ for rule in self.survey.rule_collection:
154
+ if rule.current_q == index:
155
+ continue # Remove rules associated with the deleted question
156
+ if rule.current_q > index:
157
+ rule.current_q -= 1
158
+ if rule.next_q > index:
159
+ rule.next_q -= 1
160
+
161
+ if rule.next_q == index:
162
+ if index == len(self.survey.questions):
163
+ rule.next_q = EndOfSurvey
164
+ else:
165
+ rule.next_q = index
166
+
167
+ new_rule_collection.add_rule(rule)
168
+ self.survey.rule_collection = new_rule_collection
169
+
170
+ # Update memory plan if it exists
171
+ if hasattr(self.survey, "memory_plan"):
172
+ self.survey.memory_plan.remove_question(deleted_question.question_name)
173
+
174
+ return self.survey
175
+
176
+ def add_instruction(
177
+ self, instruction: Union["Instruction", "ChangeInstruction"]
178
+ ) -> "Survey":
179
+ """
180
+ Add an instruction to the survey.
181
+
182
+ :param instruction: The instruction to add to the survey.
183
+
184
+ >>> from edsl import Instruction
185
+ >>> from edsl.surveys.Survey import Survey
186
+ >>> i = Instruction(text="Pay attention to the following questions.", name="intro")
187
+ >>> s = Survey().add_instruction(i)
188
+ >>> s._instruction_names_to_instructions
189
+ {'intro': Instruction(name="intro", text="Pay attention to the following questions.")}
190
+ >>> s._pseudo_indices
191
+ {'intro': -0.5}
192
+ """
193
+ import math
194
+
195
+ if instruction.name in self.survey._instruction_names_to_instructions:
196
+ raise SurveyCreationError(
197
+ f"""Instruction name '{instruction.name}' already exists in survey. Existing names are {self.survey._instruction_names_to_instructions.keys()}."""
198
+ )
199
+ self.survey._instruction_names_to_instructions[instruction.name] = instruction
200
+
201
+ # was the last thing added an instruction or a question?
202
+ if self.survey._pseudo_indices.last_item_was_instruction:
203
+ pseudo_index = (
204
+ self.survey._pseudo_indices.max_pseudo_index
205
+ + (
206
+ math.ceil(self.survey._pseudo_indices.max_pseudo_index)
207
+ - self.survey._pseudo_indices.max_pseudo_index
208
+ )
209
+ / 2
210
+ )
211
+ else:
212
+ pseudo_index = self.survey._pseudo_indices.max_pseudo_index + 1.0 / 2.0
213
+ self.survey._pseudo_indices[instruction.name] = pseudo_index
214
+
215
+ return self.survey
216
+
217
+
218
+ if __name__ == "__main__":
219
+ import doctest
220
+
221
+ doctest.testmod()
@@ -0,0 +1,100 @@
1
+ from typing import TYPE_CHECKING
2
+ from dataclasses import dataclass
3
+
4
+
5
+ @dataclass
6
+ class SeparatedComponents:
7
+ true_questions: list
8
+ instruction_names_to_instructions: dict
9
+ pseudo_indices: dict
10
+
11
+
12
+ class InstructionHandler:
13
+ def __init__(self, survey):
14
+ self.survey = survey
15
+
16
+ @staticmethod
17
+ def separate_questions_and_instructions(questions_and_instructions: list) -> tuple:
18
+ """
19
+ The 'pseudo_indices' attribute is a dictionary that maps question names to pseudo-indices
20
+ that are used to order questions and instructions in the survey.
21
+ Only questions get real indices; instructions get pseudo-indices.
22
+ However, the order of the pseudo-indices is the same as the order questions and instructions are added to the survey.
23
+
24
+ We don't have to know how many instructions there are to calculate the pseudo-indices because they are
25
+ calculated by the inverse of one minus the sum of 1/2^n for n in the number of instructions run so far.
26
+
27
+ >>> from edsl import Survey
28
+ >>> from edsl import Instruction
29
+ >>> i = Instruction(text = "Pay attention to the following questions.", name = "intro")
30
+ >>> i2 = Instruction(text = "How are you feeling today?", name = "followon_intro")
31
+ >>> from edsl import QuestionFreeText; q1 = QuestionFreeText.example()
32
+ >>> from edsl import QuestionMultipleChoice; q2 = QuestionMultipleChoice.example()
33
+ >>> s = Survey([q1, i, i2, q2])
34
+ >>> len(s._instruction_names_to_instructions)
35
+ 2
36
+ >>> s._pseudo_indices
37
+ {'how_are_you': 0, 'intro': 0.5, 'followon_intro': 0.75, 'how_feeling': 1}
38
+
39
+ >>> from edsl import ChangeInstruction
40
+ >>> q3 = QuestionFreeText(question_text = "What is your favorite color?", question_name = "color")
41
+ >>> i_change = ChangeInstruction(drop = ["intro"])
42
+ >>> s = Survey([q1, i, q2, i_change, q3])
43
+ >>> [i.name for i in s._relevant_instructions(q1)]
44
+ []
45
+ >>> [i.name for i in s._relevant_instructions(q2)]
46
+ ['intro']
47
+ >>> [i.name for i in s._relevant_instructions(q3)]
48
+ []
49
+
50
+ >>> i_change = ChangeInstruction(keep = ["poop"], drop = [])
51
+ >>> s = Survey([q1, i, q2, i_change])
52
+ Traceback (most recent call last):
53
+ ...
54
+ ValueError: ChangeInstruction change_instruction_0 references instruction poop which does not exist.
55
+ """
56
+ from edsl.surveys.instructions.Instruction import Instruction
57
+ from edsl.surveys.instructions.ChangeInstruction import ChangeInstruction
58
+ from edsl.questions.QuestionBase import QuestionBase
59
+
60
+ true_questions = []
61
+ instruction_names_to_instructions = {}
62
+
63
+ num_change_instructions = 0
64
+ pseudo_indices = {}
65
+ instructions_run_length = 0
66
+ for entry in questions_and_instructions:
67
+ if isinstance(entry, Instruction) or isinstance(entry, ChangeInstruction):
68
+ if isinstance(entry, ChangeInstruction):
69
+ entry.add_name(num_change_instructions)
70
+ num_change_instructions += 1
71
+ for prior_instruction in entry.keep + entry.drop:
72
+ if prior_instruction not in instruction_names_to_instructions:
73
+ raise ValueError(
74
+ f"ChangeInstruction {entry.name} references instruction {prior_instruction} which does not exist."
75
+ )
76
+ instructions_run_length += 1
77
+ delta = 1 - 1.0 / (2.0**instructions_run_length)
78
+ pseudo_index = (len(true_questions) - 1) + delta
79
+ entry.pseudo_index = pseudo_index
80
+ instruction_names_to_instructions[entry.name] = entry
81
+ elif isinstance(entry, QuestionBase):
82
+ pseudo_index = len(true_questions)
83
+ instructions_run_length = 0
84
+ true_questions.append(entry)
85
+ else:
86
+ raise ValueError(
87
+ f"Entry {repr(entry)} is not a QuestionBase or an Instruction."
88
+ )
89
+
90
+ pseudo_indices[entry.name] = pseudo_index
91
+
92
+ return SeparatedComponents(
93
+ true_questions, instruction_names_to_instructions, pseudo_indices
94
+ )
95
+
96
+
97
+ if __name__ == "__main__":
98
+ import doctest
99
+
100
+ doctest.testmod()
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+ from typing import Callable, Union, List, TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ from edsl.questions.QuestionBase import QuestionBase
6
+
7
+
8
+ class MemoryManagement:
9
+ def __init__(self, survey):
10
+ self.survey = survey
11
+
12
+ def _set_memory_plan(self, prior_questions_func: Callable) -> None:
13
+ """Set memory plan based on a provided function determining prior questions.
14
+ :param prior_questions_func: A function that takes an index and returns a list of prior questions.
15
+ """
16
+ for i, question_name in enumerate(self.survey.question_names):
17
+ self.survey.memory_plan.add_memory_collection(
18
+ focal_question=question_name,
19
+ prior_questions=prior_questions_func(i),
20
+ )
21
+
22
+ def add_targeted_memory(
23
+ self,
24
+ focal_question: Union[QuestionBase, str],
25
+ prior_question: Union[QuestionBase, str],
26
+ ) -> "Survey":
27
+ """Add instructions to a survey than when answering focal_question.
28
+
29
+ :param focal_question: The question that the agent is answering.
30
+ :param prior_question: The question that the agent should remember when answering the focal question.
31
+
32
+ Here we add instructions to a survey than when answering q2 they should remember q1:
33
+ """
34
+ focal_question_name = self.survey.question_names[
35
+ self.survey._get_question_index(focal_question)
36
+ ]
37
+ prior_question_name = self.survey.question_names[
38
+ self.survey._get_question_index(prior_question)
39
+ ]
40
+
41
+ self.survey.memory_plan.add_single_memory(
42
+ focal_question=focal_question_name,
43
+ prior_question=prior_question_name,
44
+ )
45
+
46
+ return self.survey
47
+
48
+ def add_memory_collection(
49
+ self,
50
+ focal_question: Union[QuestionBase, str],
51
+ prior_questions: List[Union[QuestionBase, str]],
52
+ ) -> "Survey":
53
+ """Add prior questions and responses so the agent has them when answering.
54
+
55
+ 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.
56
+
57
+ :param focal_question: The question that the agent is answering.
58
+ :param prior_questions: The questions that the agent should remember when answering the focal question.
59
+ """
60
+ focal_question_name = self.survey.question_names[
61
+ self.survey._get_question_index(focal_question)
62
+ ]
63
+
64
+ prior_question_names = [
65
+ self.survey.question_names[self.survey._get_question_index(prior_question)]
66
+ for prior_question in prior_questions
67
+ ]
68
+
69
+ self.survey.memory_plan.add_memory_collection(
70
+ focal_question=focal_question_name, prior_questions=prior_question_names
71
+ )
72
+ return self.survey
edsl/surveys/Rule.py CHANGED
@@ -21,13 +21,13 @@ import ast
21
21
  import random
22
22
  from typing import Any, Union, List
23
23
 
24
- from jinja2 import Template
25
- from rich import print
24
+
25
+ # from rich import print
26
26
  from simpleeval import EvalWithCompoundTypes
27
27
 
28
28
  from edsl.exceptions.surveys import SurveyError
29
29
 
30
- from edsl.exceptions import (
30
+ from edsl.exceptions.surveys import (
31
31
  SurveyRuleCannotEvaluateError,
32
32
  SurveyRuleCollectionHasNoRulesAtNodeError,
33
33
  SurveyRuleRefersToFutureStateError,
@@ -37,7 +37,7 @@ from edsl.exceptions import (
37
37
  )
38
38
  from edsl.surveys.base import EndOfSurvey
39
39
  from edsl.utilities.ast_utilities import extract_variable_names
40
- from edsl.utilities.decorators import add_edsl_version, remove_edsl_version
40
+ from edsl.utilities.remove_edsl_version import remove_edsl_version
41
41
 
42
42
 
43
43
  class QuestionIndex:
@@ -253,6 +253,7 @@ class Rule:
253
253
  ...
254
254
  edsl.exceptions.surveys.SurveyRuleCannotEvaluateError...
255
255
  """
256
+ from jinja2 import Template
256
257
 
257
258
  def substitute_in_answers(expression, current_info_env):
258
259
  """Take the dictionary of answers and substitute them into the expression."""