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
@@ -1,57 +1,78 @@
1
1
  from __future__ import annotations
2
- from typing import Dict, Any, Optional, Set
3
-
4
- from jinja2 import Environment, meta
2
+ from typing import Dict, Any, Optional, Set, Union, TYPE_CHECKING
3
+ from functools import cached_property
5
4
 
6
5
  from edsl.prompts.Prompt import Prompt
7
- from edsl.agents.prompt_helpers import PromptPlan
8
6
 
7
+ from dataclasses import dataclass
9
8
 
10
- class PlaceholderAnswer:
11
- """A placeholder answer for when a question is not yet answered."""
9
+ from .prompt_helpers import PromptPlan
10
+ from .QuestionTemplateReplacementsBuilder import (
11
+ QuestionTemplateReplacementsBuilder,
12
+ )
13
+ from .question_option_processor import QuestionOptionProcessor
12
14
 
13
- def __init__(self):
14
- self.answer = "N/A"
15
+ if TYPE_CHECKING:
16
+ from edsl.agents.InvigilatorBase import InvigilatorBase
17
+ from edsl.questions.QuestionBase import QuestionBase
18
+ from edsl.agents.Agent import Agent
19
+ from edsl.surveys.Survey import Survey
20
+ from edsl.language_models.LanguageModel import LanguageModel
21
+ from edsl.surveys.MemoryPlan import MemoryPlan
22
+ from edsl.questions.QuestionBase import QuestionBase
23
+ from edsl.scenarios.Scenario import Scenario
24
+
25
+
26
+ class BasePlaceholder:
27
+ """Base class for placeholder values when a question is not yet answered."""
28
+
29
+ def __init__(self, placeholder_type: str = "answer"):
30
+ self.value = "N/A"
15
31
  self.comment = "Will be populated by prior answer"
32
+ self._type = placeholder_type
16
33
 
17
34
  def __getitem__(self, index):
18
35
  return ""
19
36
 
20
37
  def __str__(self):
21
- return "<<PlaceholderAnswer>>"
38
+ return f"<<{self.__class__.__name__}:{self._type}>>"
22
39
 
23
40
  def __repr__(self):
24
- return "<<PlaceholderAnswer>>"
41
+ return self.__str__()
25
42
 
26
43
 
27
- def get_jinja2_variables(template_str: str) -> Set[str]:
28
- """
29
- Extracts all variable names from a Jinja2 template using Jinja2's built-in parsing.
44
+ class PlaceholderAnswer(BasePlaceholder):
45
+ def __init__(self):
46
+ super().__init__("answer")
30
47
 
31
- Args:
32
- template_str (str): The Jinja2 template string
33
48
 
34
- Returns:
35
- Set[str]: A set of variable names found in the template
36
- """
37
- env = Environment()
38
- ast = env.parse(template_str)
39
- return meta.find_undeclared_variables(ast)
49
+ class PlaceholderComment(BasePlaceholder):
50
+ def __init__(self):
51
+ super().__init__("comment")
52
+
53
+
54
+ class PlaceholderGeneratedTokens(BasePlaceholder):
55
+ def __init__(self):
56
+ super().__init__("generated_tokens")
40
57
 
41
58
 
42
59
  class PromptConstructor:
43
60
  """
61
+ This class constructs the prompts for the language model.
62
+
44
63
  The pieces of a prompt are:
45
64
  - The agent instructions - "You are answering questions as if you were a human. Do not break character."
46
65
  - The persona prompt - "You are an agent with the following persona: {'age': 22, 'hair': 'brown', 'height': 5.5}"
47
66
  - The question instructions - "You are being asked the following question: Do you like school? The options are 0: yes 1: no Return a valid JSON formatted like this, selecting only the number of the option: {"answer": <put answer code here>, "comment": "<put explanation here>"} Only 1 option may be selected."
48
67
  - The memory prompt - "Before the question you are now answering, you already answered the following question(s): Question: Do you like school? Answer: Prior answer"
49
-
50
- This is mixed into the Invigilator class.
51
68
  """
52
69
 
53
- def __init__(self, invigilator, prompt_plan: Optional["PromptPlan"] = None):
70
+ def __init__(
71
+ self, invigilator: "InvigilatorBase", prompt_plan: Optional["PromptPlan"] = None
72
+ ):
54
73
  self.invigilator = invigilator
74
+ self.prompt_plan = prompt_plan or PromptPlan()
75
+
55
76
  self.agent = invigilator.agent
56
77
  self.question = invigilator.question
57
78
  self.scenario = invigilator.scenario
@@ -59,22 +80,12 @@ class PromptConstructor:
59
80
  self.model = invigilator.model
60
81
  self.current_answers = invigilator.current_answers
61
82
  self.memory_plan = invigilator.memory_plan
62
- self.prompt_plan = prompt_plan or PromptPlan()
63
83
 
64
- @property
65
- def scenario_file_keys(self) -> list:
66
- """We need to find all the keys in the scenario that refer to FileStore objects.
67
- These will be used to append to the prompt a list of files that are part of the scenario.
68
- """
69
- from edsl.scenarios.FileStore import FileStore
70
-
71
- file_entries = []
72
- for key, value in self.scenario.items():
73
- if isinstance(value, FileStore):
74
- file_entries.append(key)
75
- return file_entries
84
+ def get_question_options(self, question_data):
85
+ """Get the question options."""
86
+ return QuestionOptionProcessor(self).get_question_options(question_data)
76
87
 
77
- @property
88
+ @cached_property
78
89
  def agent_instructions_prompt(self) -> Prompt:
79
90
  """
80
91
  >>> from edsl.agents.InvigilatorBase import InvigilatorBase
@@ -82,14 +93,14 @@ class PromptConstructor:
82
93
  >>> i.prompt_constructor.agent_instructions_prompt
83
94
  Prompt(text=\"""You are answering questions as if you were a human. Do not break character.\""")
84
95
  """
85
- from edsl import Agent
96
+ from edsl.agents.Agent import Agent
86
97
 
87
98
  if self.agent == Agent(): # if agent is empty, then return an empty prompt
88
99
  return Prompt(text="")
89
100
 
90
101
  return Prompt(text=self.agent.instruction)
91
102
 
92
- @property
103
+ @cached_property
93
104
  def agent_persona_prompt(self) -> Prompt:
94
105
  """
95
106
  >>> from edsl.agents.InvigilatorBase import InvigilatorBase
@@ -97,159 +108,96 @@ class PromptConstructor:
97
108
  >>> i.prompt_constructor.agent_persona_prompt
98
109
  Prompt(text=\"""Your traits: {'age': 22, 'hair': 'brown', 'height': 5.5}\""")
99
110
  """
100
- from edsl import Agent
111
+ from edsl.agents.Agent import Agent
101
112
 
102
113
  if self.agent == Agent(): # if agent is empty, then return an empty prompt
103
114
  return Prompt(text="")
104
115
 
105
116
  return self.agent.prompt()
106
117
 
107
- def prior_answers_dict(self) -> dict:
108
- # this is all questions
109
- d = self.survey.question_names_to_questions()
110
- # This attaches the answer to the question
111
- for question in d:
112
- if question in self.current_answers:
113
- d[question].answer = self.current_answers[question]
114
- else:
115
- d[question].answer = PlaceholderAnswer()
116
-
117
- # if (new_question := question.split("_comment")[0]) in d:
118
- # d[new_question].comment = answer
119
- # d[question].answer = PlaceholderAnswer()
120
-
121
- # breakpoint()
122
- return d
123
-
124
- @property
125
- def question_file_keys(self):
126
- raw_question_text = self.question.question_text
127
- variables = get_jinja2_variables(raw_question_text)
128
- question_file_keys = []
129
- for var in variables:
130
- if var in self.scenario_file_keys:
131
- question_file_keys.append(var)
132
- return question_file_keys
133
-
134
- def build_replacement_dict(self, question_data: dict):
118
+ def prior_answers_dict(self) -> dict[str, "QuestionBase"]:
119
+ """This is a dictionary of prior answers, if they exist."""
120
+ return self._add_answers(
121
+ self.survey.question_names_to_questions(), self.current_answers
122
+ )
123
+
124
+ @staticmethod
125
+ def _extract_quetion_and_entry_type(key_entry) -> tuple[str, str]:
135
126
  """
136
- Builds a dictionary of replacement values by combining multiple data sources.
127
+ Extracts the question name and type for the current answer dictionary key entry.
128
+
129
+ >>> PromptConstructor._extract_quetion_and_entry_type("q0")
130
+ ('q0', 'answer')
131
+ >>> PromptConstructor._extract_quetion_and_entry_type("q0_comment")
132
+ ('q0', 'comment')
133
+ >>> PromptConstructor._extract_quetion_and_entry_type("q0_alternate_generated_tokens")
134
+ ('q0_alternate', 'generated_tokens')
135
+ >>> PromptConstructor._extract_quetion_and_entry_type("q0_alt_comment")
136
+ ('q0_alt', 'comment')
137
137
  """
138
- # File references dictionary
139
- file_refs = {key: f"<see file {key}>" for key in self.scenario_file_keys}
140
-
141
- # Scenario items excluding file keys
142
- scenario_items = {
143
- k: v for k, v in self.scenario.items() if k not in self.scenario_file_keys
144
- }
145
-
146
- # Question settings with defaults
147
- question_settings = {
148
- "use_code": getattr(self.question, "_use_code", True),
149
- "include_comment": getattr(self.question, "_include_comment", False),
150
- }
151
-
152
- # Combine all dictionaries using dict.update() for clarity
153
- replacement_dict = {}
154
- for d in [
155
- file_refs,
156
- question_data,
157
- scenario_items,
158
- self.prior_answers_dict(),
159
- {"agent": self.agent},
160
- question_settings,
161
- ]:
162
- replacement_dict.update(d)
163
-
164
- return replacement_dict
165
-
166
- def _get_question_options(self, question_data):
167
- question_options_entry = question_data.get("question_options", None)
168
- question_options = question_options_entry
169
-
170
- placeholder = ["<< Option 1 - Placholder >>", "<< Option 2 - Placholder >>"]
171
-
172
- # print("Question options entry: ", question_options_entry)
173
-
174
- if isinstance(question_options_entry, str):
175
- env = Environment()
176
- parsed_content = env.parse(question_options_entry)
177
- question_option_key = list(meta.find_undeclared_variables(parsed_content))[
178
- 0
179
- ]
180
- if isinstance(self.scenario.get(question_option_key), list):
181
- question_options = self.scenario.get(question_option_key)
182
-
183
- # might be getting it from the prior answers
184
- if self.prior_answers_dict().get(question_option_key) is not None:
185
- prior_question = self.prior_answers_dict().get(question_option_key)
186
- if hasattr(prior_question, "answer"):
187
- if isinstance(prior_question.answer, list):
188
- question_options = prior_question.answer
189
- else:
190
- question_options = placeholder
191
- else:
192
- question_options = placeholder
193
-
194
- return question_options
195
-
196
- def build_question_instructions_prompt(self):
197
- """Buils the question instructions prompt."""
198
-
199
- question_prompt = Prompt(self.question.get_instructions(model=self.model.model))
200
-
201
- # Get the data for the question - this is a dictionary of the question data
202
- # e.g., {'question_text': 'Do you like school?', 'question_name': 'q0', 'question_options': ['yes', 'no']}
203
- question_data = self.question.data.copy()
204
-
205
- if (
206
- "question_options" in question_data
207
- ): # is this a question with question options?
208
- question_options = self._get_question_options(question_data)
209
- question_data["question_options"] = question_options
210
-
211
- replacement_dict = self.build_replacement_dict(question_data)
212
- rendered_instructions = question_prompt.render(replacement_dict)
138
+ split_list = key_entry.rsplit("_", maxsplit=1)
139
+ if len(split_list) == 1:
140
+ question_name = split_list[0]
141
+ entry_type = "answer"
142
+ else:
143
+ if split_list[1] == "comment":
144
+ question_name = split_list[0]
145
+ entry_type = "comment"
146
+ elif split_list[1] == "tokens": # it's actually 'generated_tokens'
147
+ question_name = key_entry.replace("_generated_tokens", "")
148
+ entry_type = "generated_tokens"
149
+ else:
150
+ question_name = key_entry
151
+ entry_type = "answer"
152
+ return question_name, entry_type
213
153
 
214
- # is there anything left to render?
215
- undefined_template_variables = (
216
- rendered_instructions.undefined_template_variables({})
217
- )
154
+ @staticmethod
155
+ def _augmented_answers_dict(current_answers: dict) -> dict:
156
+ """
157
+ >>> PromptConstructor._augmented_answers_dict({"q0": "LOVE IT!", "q0_comment": "I love school!"})
158
+ {'q0': {'answer': 'LOVE IT!', 'comment': 'I love school!'}}
159
+ """
160
+ from collections import defaultdict
218
161
 
219
- # Check if it's the name of a question in the survey
220
- for question_name in self.survey.question_names:
221
- if question_name in undefined_template_variables:
222
- print(
223
- "Question name found in undefined_template_variables: ",
224
- question_name,
225
- )
226
-
227
- if undefined_template_variables:
228
- msg = f"Question instructions still has variables: {undefined_template_variables}."
229
- import warnings
230
-
231
- warnings.warn(msg)
232
- # raise QuestionScenarioRenderError(
233
- # f"Question instructions still has variables: {undefined_template_variables}."
234
- # )
235
-
236
- # Check if question has instructions - these are instructions in a Survey that can apply to multiple follow-on questions
237
- relevant_instructions = self.survey.relevant_instructions(
238
- self.question.question_name
239
- )
162
+ d = defaultdict(dict)
163
+ for key, value in current_answers.items():
164
+ question_name, entry_type = (
165
+ PromptConstructor._extract_quetion_and_entry_type(key)
166
+ )
167
+ d[question_name][entry_type] = value
168
+ return dict(d)
240
169
 
241
- if relevant_instructions != []:
242
- # preamble_text = Prompt(
243
- # text="You were given the following instructions: "
244
- # )
245
- preamble_text = Prompt(text="")
246
- for instruction in relevant_instructions:
247
- preamble_text += instruction.text
248
- rendered_instructions = preamble_text + rendered_instructions
170
+ @staticmethod
171
+ def _add_answers(
172
+ answer_dict: dict, current_answers: dict
173
+ ) -> dict[str, "QuestionBase"]:
174
+ """
175
+ >>> from edsl import QuestionFreeText
176
+ >>> d = {"q0": QuestionFreeText(question_text="Do you like school?", question_name = "q0")}
177
+ >>> current_answers = {"q0": "LOVE IT!"}
178
+ >>> PromptConstructor._add_answers(d, current_answers)['q0'].answer
179
+ 'LOVE IT!'
180
+ """
181
+ augmented_answers = PromptConstructor._augmented_answers_dict(current_answers)
249
182
 
250
- return rendered_instructions
183
+ for question in answer_dict:
184
+ if question in augmented_answers:
185
+ for entry_type, value in augmented_answers[question].items():
186
+ setattr(answer_dict[question], entry_type, value)
187
+ else:
188
+ answer_dict[question].answer = PlaceholderAnswer()
189
+ answer_dict[question].comment = PlaceholderComment()
190
+ answer_dict[question].generated_tokens = PlaceholderGeneratedTokens()
191
+ return answer_dict
192
+
193
+ @cached_property
194
+ def question_file_keys(self) -> list:
195
+ """Extracts the file keys from the question text.
196
+ It checks if the variables in the question text are in the scenario file keys.
197
+ """
198
+ return QuestionTemplateReplacementsBuilder(self).question_file_keys()
251
199
 
252
- @property
200
+ @cached_property
253
201
  def question_instructions_prompt(self) -> Prompt:
254
202
  """
255
203
  >>> from edsl.agents.InvigilatorBase import InvigilatorBase
@@ -258,25 +206,24 @@ class PromptConstructor:
258
206
  Prompt(text=\"""...
259
207
  ...
260
208
  """
261
- if not hasattr(self, "_question_instructions_prompt"):
262
- self._question_instructions_prompt = (
263
- self.build_question_instructions_prompt()
264
- )
209
+ return self.build_question_instructions_prompt()
265
210
 
266
- return self._question_instructions_prompt
211
+ def build_question_instructions_prompt(self) -> Prompt:
212
+ """Buils the question instructions prompt."""
213
+ from edsl.agents.QuestionInstructionPromptBuilder import (
214
+ QuestionInstructionPromptBuilder,
215
+ )
267
216
 
268
- @property
269
- def prior_question_memory_prompt(self) -> Prompt:
270
- if not hasattr(self, "_prior_question_memory_prompt"):
271
- from edsl.prompts.Prompt import Prompt
217
+ return QuestionInstructionPromptBuilder(self).build()
272
218
 
273
- memory_prompt = Prompt(text="")
274
- if self.memory_plan is not None:
275
- memory_prompt += self.create_memory_prompt(
276
- self.question.question_name
277
- ).render(self.scenario | self.prior_answers_dict())
278
- self._prior_question_memory_prompt = memory_prompt
279
- return self._prior_question_memory_prompt
219
+ @cached_property
220
+ def prior_question_memory_prompt(self) -> Prompt:
221
+ memory_prompt = Prompt(text="")
222
+ if self.memory_plan is not None:
223
+ memory_prompt += self.create_memory_prompt(
224
+ self.question.question_name
225
+ ).render(self.scenario | self.prior_answers_dict())
226
+ return memory_prompt
280
227
 
281
228
  def create_memory_prompt(self, question_name: str) -> Prompt:
282
229
  """Create a memory for the agent.
@@ -295,24 +242,6 @@ class PromptConstructor:
295
242
  question_name, self.current_answers
296
243
  )
297
244
 
298
- def construct_system_prompt(self) -> Prompt:
299
- """Construct the system prompt for the LLM call."""
300
- import warnings
301
-
302
- warnings.warn(
303
- "This method is deprecated. Use get_prompts instead.", DeprecationWarning
304
- )
305
- return self.get_prompts()["system_prompt"]
306
-
307
- def construct_user_prompt(self) -> Prompt:
308
- """Construct the user prompt for the LLM call."""
309
- import warnings
310
-
311
- warnings.warn(
312
- "This method is deprecated. Use get_prompts instead.", DeprecationWarning
313
- )
314
- return self.get_prompts()["user_prompt"]
315
-
316
245
  def get_prompts(self) -> Dict[str, Prompt]:
317
246
  """Get both prompts for the LLM call.
318
247
 
@@ -323,7 +252,6 @@ class PromptConstructor:
323
252
  >>> i.get_prompts()
324
253
  {'user_prompt': ..., 'system_prompt': ...}
325
254
  """
326
- # breakpoint()
327
255
  prompts = self.prompt_plan.get_prompts(
328
256
  agent_instructions=self.agent_instructions_prompt,
329
257
  agent_persona=self.agent_persona_prompt,
@@ -337,16 +265,6 @@ class PromptConstructor:
337
265
  prompts["files_list"] = files_list
338
266
  return prompts
339
267
 
340
- def _get_scenario_with_image(self) -> Scenario:
341
- """This is a helper function to get a scenario with an image, for testing purposes."""
342
- from edsl import Scenario
343
-
344
- try:
345
- scenario = Scenario.from_image("../../static/logo.png")
346
- except FileNotFoundError:
347
- scenario = Scenario.from_image("static/logo.png")
348
- return scenario
349
-
350
268
 
351
269
  if __name__ == "__main__":
352
270
  import doctest
@@ -0,0 +1,128 @@
1
+ from typing import Dict, List, Set
2
+ from warnings import warn
3
+ from edsl.prompts.Prompt import Prompt
4
+
5
+ from edsl.agents.QuestionTemplateReplacementsBuilder import (
6
+ QuestionTemplateReplacementsBuilder as QTRB,
7
+ )
8
+
9
+
10
+ class QuestionInstructionPromptBuilder:
11
+ """Handles the construction and rendering of question instructions."""
12
+
13
+ def __init__(self, prompt_constructor: "PromptConstructor"):
14
+ self.prompt_constructor = prompt_constructor
15
+
16
+ self.model = self.prompt_constructor.model
17
+ self.survey = self.prompt_constructor.survey
18
+ self.question = self.prompt_constructor.question
19
+
20
+ def build(self) -> Prompt:
21
+ """Builds the complete question instructions prompt with all necessary components.
22
+
23
+ Returns:
24
+ Prompt: The fully rendered question instructions
25
+ """
26
+ base_prompt = self._create_base_prompt()
27
+ enriched_prompt = self._enrich_with_question_options(base_prompt)
28
+ rendered_prompt = self._render_prompt(enriched_prompt)
29
+ self._validate_template_variables(rendered_prompt)
30
+
31
+ return self._append_survey_instructions(rendered_prompt)
32
+
33
+ def _create_base_prompt(self) -> Dict:
34
+ """Creates the initial prompt with basic question data.
35
+
36
+ Returns:
37
+ Dict: Base question data
38
+ """
39
+ return {
40
+ "prompt": Prompt(self.question.get_instructions(model=self.model.model)),
41
+ "data": self.question.data.copy(),
42
+ }
43
+
44
+ def _enrich_with_question_options(self, prompt_data: Dict) -> Dict:
45
+ """Enriches the prompt data with question options if they exist.
46
+
47
+ Args:
48
+ prompt_data: Dictionary containing prompt and question data
49
+
50
+ Returns:
51
+ Dict: Enriched prompt data
52
+ """
53
+ if "question_options" in prompt_data["data"]:
54
+ from edsl.agents.question_option_processor import QuestionOptionProcessor
55
+
56
+ question_options = QuestionOptionProcessor(
57
+ self.prompt_constructor
58
+ ).get_question_options(question_data=prompt_data["data"])
59
+
60
+ prompt_data["data"]["question_options"] = question_options
61
+ return prompt_data
62
+
63
+ def _render_prompt(self, prompt_data: Dict) -> Prompt:
64
+ """Renders the prompt using the replacement dictionary.
65
+
66
+ Args:
67
+ prompt_data: Dictionary containing prompt and question data
68
+
69
+ Returns:
70
+ Prompt: Rendered instructions
71
+ """
72
+
73
+ replacement_dict = QTRB(self.prompt_constructor).build_replacement_dict(
74
+ prompt_data["data"]
75
+ )
76
+ return prompt_data["prompt"].render(replacement_dict)
77
+
78
+ def _validate_template_variables(self, rendered_prompt: Prompt) -> None:
79
+ """Validates that all template variables have been properly replaced.
80
+
81
+ Args:
82
+ rendered_prompt: The rendered prompt to validate
83
+
84
+ Warns:
85
+ If any template variables remain undefined
86
+ """
87
+ undefined_vars = rendered_prompt.undefined_template_variables({})
88
+
89
+ # Check for question names in undefined variables
90
+ self._check_question_names_in_undefined_vars(undefined_vars)
91
+
92
+ # Warn about any remaining undefined variables
93
+ if undefined_vars:
94
+ warn(f"Question instructions still has variables: {undefined_vars}.")
95
+
96
+ def _check_question_names_in_undefined_vars(self, undefined_vars: Set[str]) -> None:
97
+ """Checks if any undefined variables match question names in the survey.
98
+
99
+ Args:
100
+ undefined_vars: Set of undefined template variables
101
+ """
102
+ for question_name in self.survey.question_names:
103
+ if question_name in undefined_vars:
104
+ print(
105
+ f"Question name found in undefined_template_variables: {question_name}"
106
+ )
107
+
108
+ def _append_survey_instructions(self, rendered_prompt: Prompt) -> Prompt:
109
+ """Appends any relevant survey instructions to the rendered prompt.
110
+
111
+ Args:
112
+ rendered_prompt: The rendered prompt to append instructions to
113
+
114
+ Returns:
115
+ Prompt: Final prompt with survey instructions
116
+ """
117
+ relevant_instructions = self.survey._relevant_instructions(
118
+ self.question.question_name
119
+ )
120
+
121
+ if not relevant_instructions:
122
+ return rendered_prompt
123
+
124
+ preamble = Prompt(text="")
125
+ for instruction in relevant_instructions:
126
+ preamble += instruction.text
127
+
128
+ return preamble + rendered_prompt