edsl 0.1.36.dev7__py3-none-any.whl → 0.1.37.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. edsl/Base.py +303 -303
  2. edsl/BaseDiff.py +260 -260
  3. edsl/TemplateLoader.py +24 -24
  4. edsl/__init__.py +48 -48
  5. edsl/__version__.py +1 -1
  6. edsl/agents/Agent.py +804 -804
  7. edsl/agents/AgentList.py +345 -337
  8. edsl/agents/Invigilator.py +222 -222
  9. edsl/agents/InvigilatorBase.py +305 -298
  10. edsl/agents/PromptConstructor.py +310 -320
  11. edsl/agents/__init__.py +3 -3
  12. edsl/agents/descriptors.py +86 -86
  13. edsl/agents/prompt_helpers.py +129 -129
  14. edsl/auto/AutoStudy.py +117 -117
  15. edsl/auto/StageBase.py +230 -230
  16. edsl/auto/StageGenerateSurvey.py +178 -178
  17. edsl/auto/StageLabelQuestions.py +125 -125
  18. edsl/auto/StagePersona.py +61 -61
  19. edsl/auto/StagePersonaDimensionValueRanges.py +88 -88
  20. edsl/auto/StagePersonaDimensionValues.py +74 -74
  21. edsl/auto/StagePersonaDimensions.py +69 -69
  22. edsl/auto/StageQuestions.py +73 -73
  23. edsl/auto/SurveyCreatorPipeline.py +21 -21
  24. edsl/auto/utilities.py +224 -224
  25. edsl/base/Base.py +289 -289
  26. edsl/config.py +149 -149
  27. edsl/conjure/AgentConstructionMixin.py +152 -152
  28. edsl/conjure/Conjure.py +62 -62
  29. edsl/conjure/InputData.py +659 -659
  30. edsl/conjure/InputDataCSV.py +48 -48
  31. edsl/conjure/InputDataMixinQuestionStats.py +182 -182
  32. edsl/conjure/InputDataPyRead.py +91 -91
  33. edsl/conjure/InputDataSPSS.py +8 -8
  34. edsl/conjure/InputDataStata.py +8 -8
  35. edsl/conjure/QuestionOptionMixin.py +76 -76
  36. edsl/conjure/QuestionTypeMixin.py +23 -23
  37. edsl/conjure/RawQuestion.py +65 -65
  38. edsl/conjure/SurveyResponses.py +7 -7
  39. edsl/conjure/__init__.py +9 -9
  40. edsl/conjure/naming_utilities.py +263 -263
  41. edsl/conjure/utilities.py +201 -201
  42. edsl/conversation/Conversation.py +238 -238
  43. edsl/conversation/car_buying.py +58 -58
  44. edsl/conversation/mug_negotiation.py +81 -81
  45. edsl/conversation/next_speaker_utilities.py +93 -93
  46. edsl/coop/PriceFetcher.py +54 -54
  47. edsl/coop/__init__.py +2 -2
  48. edsl/coop/coop.py +824 -849
  49. edsl/coop/utils.py +131 -131
  50. edsl/data/Cache.py +527 -527
  51. edsl/data/CacheEntry.py +228 -228
  52. edsl/data/CacheHandler.py +149 -149
  53. edsl/data/RemoteCacheSync.py +97 -84
  54. edsl/data/SQLiteDict.py +292 -292
  55. edsl/data/__init__.py +4 -4
  56. edsl/data/orm.py +10 -10
  57. edsl/data_transfer_models.py +73 -73
  58. edsl/enums.py +173 -173
  59. edsl/exceptions/__init__.py +50 -50
  60. edsl/exceptions/agents.py +40 -40
  61. edsl/exceptions/configuration.py +16 -16
  62. edsl/exceptions/coop.py +10 -10
  63. edsl/exceptions/data.py +14 -14
  64. edsl/exceptions/general.py +34 -34
  65. edsl/exceptions/jobs.py +33 -33
  66. edsl/exceptions/language_models.py +63 -63
  67. edsl/exceptions/prompts.py +15 -15
  68. edsl/exceptions/questions.py +91 -91
  69. edsl/exceptions/results.py +26 -26
  70. edsl/exceptions/surveys.py +34 -34
  71. edsl/inference_services/AnthropicService.py +87 -87
  72. edsl/inference_services/AwsBedrock.py +115 -115
  73. edsl/inference_services/AzureAI.py +217 -217
  74. edsl/inference_services/DeepInfraService.py +18 -18
  75. edsl/inference_services/GoogleService.py +156 -156
  76. edsl/inference_services/GroqService.py +20 -20
  77. edsl/inference_services/InferenceServiceABC.py +147 -147
  78. edsl/inference_services/InferenceServicesCollection.py +74 -74
  79. edsl/inference_services/MistralAIService.py +123 -123
  80. edsl/inference_services/OllamaService.py +18 -18
  81. edsl/inference_services/OpenAIService.py +224 -224
  82. edsl/inference_services/TestService.py +89 -89
  83. edsl/inference_services/TogetherAIService.py +170 -170
  84. edsl/inference_services/models_available_cache.py +118 -118
  85. edsl/inference_services/rate_limits_cache.py +25 -25
  86. edsl/inference_services/registry.py +39 -39
  87. edsl/inference_services/write_available.py +10 -10
  88. edsl/jobs/Answers.py +56 -56
  89. edsl/jobs/Jobs.py +1112 -1112
  90. edsl/jobs/__init__.py +1 -1
  91. edsl/jobs/buckets/BucketCollection.py +63 -63
  92. edsl/jobs/buckets/ModelBuckets.py +65 -65
  93. edsl/jobs/buckets/TokenBucket.py +248 -248
  94. edsl/jobs/interviews/Interview.py +661 -661
  95. edsl/jobs/interviews/InterviewExceptionCollection.py +99 -99
  96. edsl/jobs/interviews/InterviewExceptionEntry.py +182 -189
  97. edsl/jobs/interviews/InterviewStatistic.py +63 -63
  98. edsl/jobs/interviews/InterviewStatisticsCollection.py +25 -25
  99. edsl/jobs/interviews/InterviewStatusDictionary.py +78 -78
  100. edsl/jobs/interviews/InterviewStatusLog.py +92 -92
  101. edsl/jobs/interviews/ReportErrors.py +66 -66
  102. edsl/jobs/interviews/interview_status_enum.py +9 -9
  103. edsl/jobs/runners/JobsRunnerAsyncio.py +338 -337
  104. edsl/jobs/runners/JobsRunnerStatus.py +332 -332
  105. edsl/jobs/tasks/QuestionTaskCreator.py +242 -242
  106. edsl/jobs/tasks/TaskCreators.py +64 -64
  107. edsl/jobs/tasks/TaskHistory.py +441 -441
  108. edsl/jobs/tasks/TaskStatusLog.py +23 -23
  109. edsl/jobs/tasks/task_status_enum.py +163 -163
  110. edsl/jobs/tokens/InterviewTokenUsage.py +27 -27
  111. edsl/jobs/tokens/TokenUsage.py +34 -34
  112. edsl/language_models/LanguageModel.py +718 -718
  113. edsl/language_models/ModelList.py +102 -102
  114. edsl/language_models/RegisterLanguageModelsMeta.py +184 -184
  115. edsl/language_models/__init__.py +2 -2
  116. edsl/language_models/fake_openai_call.py +15 -15
  117. edsl/language_models/fake_openai_service.py +61 -61
  118. edsl/language_models/registry.py +137 -137
  119. edsl/language_models/repair.py +156 -156
  120. edsl/language_models/unused/ReplicateBase.py +83 -83
  121. edsl/language_models/utilities.py +64 -64
  122. edsl/notebooks/Notebook.py +259 -259
  123. edsl/notebooks/__init__.py +1 -1
  124. edsl/prompts/Prompt.py +350 -358
  125. edsl/prompts/__init__.py +2 -2
  126. edsl/questions/AnswerValidatorMixin.py +289 -289
  127. edsl/questions/QuestionBase.py +616 -616
  128. edsl/questions/QuestionBaseGenMixin.py +161 -161
  129. edsl/questions/QuestionBasePromptsMixin.py +266 -266
  130. edsl/questions/QuestionBudget.py +227 -227
  131. edsl/questions/QuestionCheckBox.py +359 -359
  132. edsl/questions/QuestionExtract.py +183 -183
  133. edsl/questions/QuestionFreeText.py +113 -113
  134. edsl/questions/QuestionFunctional.py +159 -159
  135. edsl/questions/QuestionList.py +231 -231
  136. edsl/questions/QuestionMultipleChoice.py +286 -286
  137. edsl/questions/QuestionNumerical.py +153 -153
  138. edsl/questions/QuestionRank.py +324 -324
  139. edsl/questions/Quick.py +41 -41
  140. edsl/questions/RegisterQuestionsMeta.py +71 -71
  141. edsl/questions/ResponseValidatorABC.py +174 -174
  142. edsl/questions/SimpleAskMixin.py +73 -73
  143. edsl/questions/__init__.py +26 -26
  144. edsl/questions/compose_questions.py +98 -98
  145. edsl/questions/decorators.py +21 -21
  146. edsl/questions/derived/QuestionLikertFive.py +76 -76
  147. edsl/questions/derived/QuestionLinearScale.py +87 -87
  148. edsl/questions/derived/QuestionTopK.py +91 -91
  149. edsl/questions/derived/QuestionYesNo.py +82 -82
  150. edsl/questions/descriptors.py +418 -418
  151. edsl/questions/prompt_templates/question_budget.jinja +13 -13
  152. edsl/questions/prompt_templates/question_checkbox.jinja +32 -32
  153. edsl/questions/prompt_templates/question_extract.jinja +11 -11
  154. edsl/questions/prompt_templates/question_free_text.jinja +3 -3
  155. edsl/questions/prompt_templates/question_linear_scale.jinja +11 -11
  156. edsl/questions/prompt_templates/question_list.jinja +17 -17
  157. edsl/questions/prompt_templates/question_multiple_choice.jinja +33 -33
  158. edsl/questions/prompt_templates/question_numerical.jinja +36 -36
  159. edsl/questions/question_registry.py +147 -147
  160. edsl/questions/settings.py +12 -12
  161. edsl/questions/templates/budget/answering_instructions.jinja +7 -7
  162. edsl/questions/templates/budget/question_presentation.jinja +7 -7
  163. edsl/questions/templates/checkbox/answering_instructions.jinja +10 -10
  164. edsl/questions/templates/checkbox/question_presentation.jinja +22 -22
  165. edsl/questions/templates/extract/answering_instructions.jinja +7 -7
  166. edsl/questions/templates/likert_five/answering_instructions.jinja +10 -10
  167. edsl/questions/templates/likert_five/question_presentation.jinja +11 -11
  168. edsl/questions/templates/linear_scale/answering_instructions.jinja +5 -5
  169. edsl/questions/templates/linear_scale/question_presentation.jinja +5 -5
  170. edsl/questions/templates/list/answering_instructions.jinja +3 -3
  171. edsl/questions/templates/list/question_presentation.jinja +5 -5
  172. edsl/questions/templates/multiple_choice/answering_instructions.jinja +9 -9
  173. edsl/questions/templates/multiple_choice/question_presentation.jinja +11 -11
  174. edsl/questions/templates/numerical/answering_instructions.jinja +6 -6
  175. edsl/questions/templates/numerical/question_presentation.jinja +6 -6
  176. edsl/questions/templates/rank/answering_instructions.jinja +11 -11
  177. edsl/questions/templates/rank/question_presentation.jinja +15 -15
  178. edsl/questions/templates/top_k/answering_instructions.jinja +8 -8
  179. edsl/questions/templates/top_k/question_presentation.jinja +22 -22
  180. edsl/questions/templates/yes_no/answering_instructions.jinja +6 -6
  181. edsl/questions/templates/yes_no/question_presentation.jinja +11 -11
  182. edsl/results/Dataset.py +293 -293
  183. edsl/results/DatasetExportMixin.py +693 -693
  184. edsl/results/DatasetTree.py +145 -145
  185. edsl/results/Result.py +435 -433
  186. edsl/results/Results.py +1160 -1158
  187. edsl/results/ResultsDBMixin.py +238 -238
  188. edsl/results/ResultsExportMixin.py +43 -43
  189. edsl/results/ResultsFetchMixin.py +33 -33
  190. edsl/results/ResultsGGMixin.py +121 -121
  191. edsl/results/ResultsToolsMixin.py +98 -98
  192. edsl/results/Selector.py +118 -118
  193. edsl/results/__init__.py +2 -2
  194. edsl/results/tree_explore.py +115 -115
  195. edsl/scenarios/FileStore.py +458 -458
  196. edsl/scenarios/Scenario.py +510 -510
  197. edsl/scenarios/ScenarioHtmlMixin.py +59 -59
  198. edsl/scenarios/ScenarioList.py +1101 -1101
  199. edsl/scenarios/ScenarioListExportMixin.py +52 -52
  200. edsl/scenarios/ScenarioListPdfMixin.py +261 -261
  201. edsl/scenarios/__init__.py +4 -4
  202. edsl/shared.py +1 -1
  203. edsl/study/ObjectEntry.py +173 -173
  204. edsl/study/ProofOfWork.py +113 -113
  205. edsl/study/SnapShot.py +80 -80
  206. edsl/study/Study.py +528 -528
  207. edsl/study/__init__.py +4 -4
  208. edsl/surveys/DAG.py +148 -148
  209. edsl/surveys/Memory.py +31 -31
  210. edsl/surveys/MemoryPlan.py +244 -244
  211. edsl/surveys/Rule.py +324 -324
  212. edsl/surveys/RuleCollection.py +387 -387
  213. edsl/surveys/Survey.py +1772 -1772
  214. edsl/surveys/SurveyCSS.py +261 -261
  215. edsl/surveys/SurveyExportMixin.py +259 -259
  216. edsl/surveys/SurveyFlowVisualizationMixin.py +121 -121
  217. edsl/surveys/SurveyQualtricsImport.py +284 -284
  218. edsl/surveys/__init__.py +3 -3
  219. edsl/surveys/base.py +53 -53
  220. edsl/surveys/descriptors.py +56 -56
  221. edsl/surveys/instructions/ChangeInstruction.py +47 -47
  222. edsl/surveys/instructions/Instruction.py +51 -51
  223. edsl/surveys/instructions/InstructionCollection.py +77 -77
  224. edsl/templates/error_reporting/base.html +23 -23
  225. edsl/templates/error_reporting/exceptions_by_model.html +34 -34
  226. edsl/templates/error_reporting/exceptions_by_question_name.html +16 -16
  227. edsl/templates/error_reporting/exceptions_by_type.html +16 -16
  228. edsl/templates/error_reporting/interview_details.html +115 -115
  229. edsl/templates/error_reporting/interviews.html +9 -9
  230. edsl/templates/error_reporting/overview.html +4 -4
  231. edsl/templates/error_reporting/performance_plot.html +1 -1
  232. edsl/templates/error_reporting/report.css +73 -73
  233. edsl/templates/error_reporting/report.html +117 -117
  234. edsl/templates/error_reporting/report.js +25 -25
  235. edsl/tools/__init__.py +1 -1
  236. edsl/tools/clusters.py +192 -192
  237. edsl/tools/embeddings.py +27 -27
  238. edsl/tools/embeddings_plotting.py +118 -118
  239. edsl/tools/plotting.py +112 -112
  240. edsl/tools/summarize.py +18 -18
  241. edsl/utilities/SystemInfo.py +28 -28
  242. edsl/utilities/__init__.py +22 -22
  243. edsl/utilities/ast_utilities.py +25 -25
  244. edsl/utilities/data/Registry.py +6 -6
  245. edsl/utilities/data/__init__.py +1 -1
  246. edsl/utilities/data/scooter_results.json +1 -1
  247. edsl/utilities/decorators.py +77 -77
  248. edsl/utilities/gcp_bucket/cloud_storage.py +96 -96
  249. edsl/utilities/interface.py +627 -627
  250. edsl/utilities/repair_functions.py +28 -28
  251. edsl/utilities/restricted_python.py +70 -70
  252. edsl/utilities/utilities.py +391 -391
  253. {edsl-0.1.36.dev7.dist-info → edsl-0.1.37.dev1.dist-info}/LICENSE +21 -21
  254. {edsl-0.1.36.dev7.dist-info → edsl-0.1.37.dev1.dist-info}/METADATA +1 -1
  255. edsl-0.1.37.dev1.dist-info/RECORD +279 -0
  256. edsl-0.1.36.dev7.dist-info/RECORD +0 -279
  257. {edsl-0.1.36.dev7.dist-info → edsl-0.1.37.dev1.dist-info}/WHEEL +0 -0
@@ -1,320 +1,310 @@
1
- from __future__ import annotations
2
- from typing import Dict, Any, Optional, Set
3
-
4
- from jinja2 import Environment, meta
5
-
6
- from edsl.prompts.Prompt import Prompt
7
-
8
- from edsl.agents.prompt_helpers import PromptPlan
9
-
10
-
11
- def get_jinja2_variables(template_str: str) -> Set[str]:
12
- """
13
- Extracts all variable names from a Jinja2 template using Jinja2's built-in parsing.
14
-
15
- Args:
16
- template_str (str): The Jinja2 template string
17
-
18
- Returns:
19
- Set[str]: A set of variable names found in the template
20
- """
21
- env = Environment()
22
- ast = env.parse(template_str)
23
- return meta.find_undeclared_variables(ast)
24
-
25
-
26
- class PromptConstructor:
27
- """
28
- The pieces of a prompt are:
29
- - The agent instructions - "You are answering questions as if you were a human. Do not break character."
30
- - The persona prompt - "You are an agent with the following persona: {'age': 22, 'hair': 'brown', 'height': 5.5}"
31
- - 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."
32
- - 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"
33
-
34
- This is mixed into the Invigilator class.
35
- """
36
-
37
- def __init__(self, invigilator):
38
- self.invigilator = invigilator
39
- self.agent = invigilator.agent
40
- self.question = invigilator.question
41
- self.scenario = invigilator.scenario
42
- self.survey = invigilator.survey
43
- self.model = invigilator.model
44
- self.current_answers = invigilator.current_answers
45
- self.memory_plan = invigilator.memory_plan
46
- self.prompt_plan = PromptPlan()
47
-
48
- @property
49
- def scenario_file_keys(self) -> list:
50
- """We need to find all the keys in the scenario that refer to FileStore objects.
51
- These will be used to append to the prompt a list of files that are part of the scenario.
52
- """
53
- from edsl.scenarios.FileStore import FileStore
54
-
55
- file_entries = []
56
- for key, value in self.scenario.items():
57
- if isinstance(value, FileStore):
58
- file_entries.append(key)
59
- return file_entries
60
-
61
- @property
62
- def agent_instructions_prompt(self) -> Prompt:
63
- """
64
- >>> from edsl.agents.InvigilatorBase import InvigilatorBase
65
- >>> i = InvigilatorBase.example()
66
- >>> i.prompt_constructor.agent_instructions_prompt
67
- Prompt(text=\"""You are answering questions as if you were a human. Do not break character.\""")
68
- """
69
- from edsl import Agent
70
-
71
- if self.agent == Agent(): # if agent is empty, then return an empty prompt
72
- return Prompt(text="")
73
-
74
- return Prompt(text=self.agent.instruction)
75
-
76
- @property
77
- def agent_persona_prompt(self) -> Prompt:
78
- """
79
- >>> from edsl.agents.InvigilatorBase import InvigilatorBase
80
- >>> i = InvigilatorBase.example()
81
- >>> i.prompt_constructor.agent_persona_prompt
82
- Prompt(text=\"""Your traits: {'age': 22, 'hair': 'brown', 'height': 5.5}\""")
83
- """
84
- from edsl import Agent
85
-
86
- if self.agent == Agent(): # if agent is empty, then return an empty prompt
87
- return Prompt(text="")
88
-
89
- return self.agent.prompt()
90
-
91
- def prior_answers_dict(self) -> dict:
92
- d = self.survey.question_names_to_questions()
93
- # This attaches the answer to the question
94
- for question, answer in self.current_answers.items():
95
- if question in d:
96
- d[question].answer = answer
97
- else:
98
- # adds a comment to the question
99
- if (new_question := question.split("_comment")[0]) in d:
100
- d[new_question].comment = answer
101
- return d
102
-
103
- @property
104
- def question_file_keys(self):
105
- raw_question_text = self.question.question_text
106
- variables = get_jinja2_variables(raw_question_text)
107
- question_file_keys = []
108
- for var in variables:
109
- if var in self.scenario_file_keys:
110
- question_file_keys.append(var)
111
- return question_file_keys
112
-
113
- @property
114
- def question_instructions_prompt(self) -> Prompt:
115
- """
116
- >>> from edsl.agents.InvigilatorBase import InvigilatorBase
117
- >>> i = InvigilatorBase.example()
118
- >>> i.prompt_constructor.question_instructions_prompt
119
- Prompt(text=\"""...
120
- ...
121
- """
122
- # The user might have passed a custom prompt, which would be stored in _question_instructions_prompt
123
- if not hasattr(self, "_question_instructions_prompt"):
124
- # Gets the instructions for the question - this is how the question should be answered
125
- question_prompt = self.question.get_instructions(model=self.model.model)
126
-
127
- # Get the data for the question - this is a dictionary of the question data
128
- # e.g., {'question_text': 'Do you like school?', 'question_name': 'q0', 'question_options': ['yes', 'no']}
129
- question_data = self.question.data.copy()
130
-
131
- # check to see if the question_options is actually a string
132
- # This is used when the user is using the question_options as a variable from a scenario
133
- # if "question_options" in question_data:
134
- if isinstance(self.question.data.get("question_options", None), str):
135
- env = Environment()
136
- parsed_content = env.parse(self.question.data["question_options"])
137
- question_option_key = list(
138
- meta.find_undeclared_variables(parsed_content)
139
- )[0]
140
-
141
- # look to see if the question_option_key is in the scenario
142
- if isinstance(
143
- question_options := self.scenario.get(question_option_key), list
144
- ):
145
- question_data["question_options"] = question_options
146
- self.question.question_options = question_options
147
-
148
- # might be getting it from the prior answers
149
- if self.prior_answers_dict().get(question_option_key) is not None:
150
- prior_question = self.prior_answers_dict().get(question_option_key)
151
- if hasattr(prior_question, "answer"):
152
- if isinstance(prior_question.answer, list):
153
- question_data["question_options"] = prior_question.answer
154
- self.question.question_options = prior_question.answer
155
- else:
156
- placeholder_options = [
157
- "N/A",
158
- "Will be populated by prior answer",
159
- "These are placeholder options",
160
- ]
161
- question_data["question_options"] = placeholder_options
162
- self.question.question_options = placeholder_options
163
-
164
- # if isinstance(
165
- # question_options := self.prior_answers_dict()
166
- # .get(question_option_key)
167
- # .answer,
168
- # list,
169
- # ):
170
- # question_data["question_options"] = question_options
171
- # self.question.question_options = question_options
172
-
173
- replacement_dict = (
174
- {key: f"<see file {key}>" for key in self.scenario_file_keys}
175
- | question_data
176
- | {
177
- k: v
178
- for k, v in self.scenario.items()
179
- if k not in self.scenario_file_keys
180
- } # don't include images in the replacement dict
181
- | self.prior_answers_dict()
182
- | {"agent": self.agent}
183
- | {
184
- "use_code": getattr(self.question, "_use_code", True),
185
- "include_comment": getattr(
186
- self.question, "_include_comment", False
187
- ),
188
- }
189
- )
190
-
191
- rendered_instructions = question_prompt.render(replacement_dict)
192
-
193
- # is there anything left to render?
194
- undefined_template_variables = (
195
- rendered_instructions.undefined_template_variables({})
196
- )
197
-
198
- # Check if it's the name of a question in the survey
199
- for question_name in self.survey.question_names:
200
- if question_name in undefined_template_variables:
201
- print(
202
- "Question name found in undefined_template_variables: ",
203
- question_name,
204
- )
205
-
206
- if undefined_template_variables:
207
- msg = f"Question instructions still has variables: {undefined_template_variables}."
208
- import warnings
209
-
210
- warnings.warn(msg)
211
- # raise QuestionScenarioRenderError(
212
- # f"Question instructions still has variables: {undefined_template_variables}."
213
- # )
214
-
215
- ####################################
216
- # Check if question has instructions - these are instructions in a Survey that can apply to multiple follow-on questions
217
- ####################################
218
- relevant_instructions = self.survey.relevant_instructions(
219
- self.question.question_name
220
- )
221
-
222
- if relevant_instructions != []:
223
- # preamble_text = Prompt(
224
- # text="You were given the following instructions: "
225
- # )
226
- preamble_text = Prompt(text="")
227
- for instruction in relevant_instructions:
228
- preamble_text += instruction.text
229
- rendered_instructions = preamble_text + rendered_instructions
230
-
231
- self._question_instructions_prompt = rendered_instructions
232
- return self._question_instructions_prompt
233
-
234
- @property
235
- def prior_question_memory_prompt(self) -> Prompt:
236
- if not hasattr(self, "_prior_question_memory_prompt"):
237
- from edsl.prompts.Prompt import Prompt
238
-
239
- memory_prompt = Prompt(text="")
240
- if self.memory_plan is not None:
241
- memory_prompt += self.create_memory_prompt(
242
- self.question.question_name
243
- ).render(self.scenario | self.prior_answers_dict())
244
- self._prior_question_memory_prompt = memory_prompt
245
- return self._prior_question_memory_prompt
246
-
247
- def create_memory_prompt(self, question_name: str) -> Prompt:
248
- """Create a memory for the agent.
249
-
250
- The returns a memory prompt for the agent.
251
-
252
- >>> from edsl.agents.InvigilatorBase import InvigilatorBase
253
- >>> i = InvigilatorBase.example()
254
- >>> i.current_answers = {"q0": "Prior answer"}
255
- >>> i.memory_plan.add_single_memory("q1", "q0")
256
- >>> p = i.prompt_constructor.create_memory_prompt("q1")
257
- >>> p.text.strip().replace("\\n", " ").replace("\\t", " ")
258
- 'Before the question you are now answering, you already answered the following question(s): Question: Do you like school? Answer: Prior answer'
259
- """
260
- return self.memory_plan.get_memory_prompt_fragment(
261
- question_name, self.current_answers
262
- )
263
-
264
- def construct_system_prompt(self) -> Prompt:
265
- """Construct the system prompt for the LLM call."""
266
- import warnings
267
-
268
- warnings.warn(
269
- "This method is deprecated. Use get_prompts instead.", DeprecationWarning
270
- )
271
- return self.get_prompts()["system_prompt"]
272
-
273
- def construct_user_prompt(self) -> Prompt:
274
- """Construct the user prompt for the LLM call."""
275
- import warnings
276
-
277
- warnings.warn(
278
- "This method is deprecated. Use get_prompts instead.", DeprecationWarning
279
- )
280
- return self.get_prompts()["user_prompt"]
281
-
282
- def get_prompts(self) -> Dict[str, Prompt]:
283
- """Get both prompts for the LLM call.
284
-
285
- >>> from edsl import QuestionFreeText
286
- >>> from edsl.agents.InvigilatorBase import InvigilatorBase
287
- >>> q = QuestionFreeText(question_text="How are you today?", question_name="q_new")
288
- >>> i = InvigilatorBase.example(question = q)
289
- >>> i.get_prompts()
290
- {'user_prompt': ..., 'system_prompt': ...}
291
- """
292
- # breakpoint()
293
- prompts = self.prompt_plan.get_prompts(
294
- agent_instructions=self.agent_instructions_prompt,
295
- agent_persona=self.agent_persona_prompt,
296
- question_instructions=self.question_instructions_prompt,
297
- prior_question_memory=self.prior_question_memory_prompt,
298
- )
299
- if self.question_file_keys:
300
- files_list = []
301
- for key in self.question_file_keys:
302
- files_list.append(self.scenario[key])
303
- prompts["files_list"] = files_list
304
- return prompts
305
-
306
- def _get_scenario_with_image(self) -> Scenario:
307
- """This is a helper function to get a scenario with an image, for testing purposes."""
308
- from edsl import Scenario
309
-
310
- try:
311
- scenario = Scenario.from_image("../../static/logo.png")
312
- except FileNotFoundError:
313
- scenario = Scenario.from_image("static/logo.png")
314
- return scenario
315
-
316
-
317
- if __name__ == "__main__":
318
- import doctest
319
-
320
- doctest.testmod(optionflags=doctest.ELLIPSIS)
1
+ from __future__ import annotations
2
+ from typing import Dict, Any, Optional, Set
3
+
4
+ from jinja2 import Environment, meta
5
+
6
+ from edsl.prompts.Prompt import Prompt
7
+ from edsl.agents.prompt_helpers import PromptPlan
8
+
9
+
10
+ def get_jinja2_variables(template_str: str) -> Set[str]:
11
+ """
12
+ Extracts all variable names from a Jinja2 template using Jinja2's built-in parsing.
13
+
14
+ Args:
15
+ template_str (str): The Jinja2 template string
16
+
17
+ Returns:
18
+ Set[str]: A set of variable names found in the template
19
+ """
20
+ env = Environment()
21
+ ast = env.parse(template_str)
22
+ return meta.find_undeclared_variables(ast)
23
+
24
+
25
+ class PromptConstructor:
26
+ """
27
+ The pieces of a prompt are:
28
+ - The agent instructions - "You are answering questions as if you were a human. Do not break character."
29
+ - The persona prompt - "You are an agent with the following persona: {'age': 22, 'hair': 'brown', 'height': 5.5}"
30
+ - 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."
31
+ - 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"
32
+
33
+ This is mixed into the Invigilator class.
34
+ """
35
+
36
+ def __init__(self, invigilator, prompt_plan: Optional["PromptPlan"] = None):
37
+ self.invigilator = invigilator
38
+ self.agent = invigilator.agent
39
+ self.question = invigilator.question
40
+ self.scenario = invigilator.scenario
41
+ self.survey = invigilator.survey
42
+ self.model = invigilator.model
43
+ self.current_answers = invigilator.current_answers
44
+ self.memory_plan = invigilator.memory_plan
45
+ self.prompt_plan = prompt_plan or PromptPlan()
46
+
47
+ @property
48
+ def scenario_file_keys(self) -> list:
49
+ """We need to find all the keys in the scenario that refer to FileStore objects.
50
+ These will be used to append to the prompt a list of files that are part of the scenario.
51
+ """
52
+ from edsl.scenarios.FileStore import FileStore
53
+
54
+ file_entries = []
55
+ for key, value in self.scenario.items():
56
+ if isinstance(value, FileStore):
57
+ file_entries.append(key)
58
+ return file_entries
59
+
60
+ @property
61
+ def agent_instructions_prompt(self) -> Prompt:
62
+ """
63
+ >>> from edsl.agents.InvigilatorBase import InvigilatorBase
64
+ >>> i = InvigilatorBase.example()
65
+ >>> i.prompt_constructor.agent_instructions_prompt
66
+ Prompt(text=\"""You are answering questions as if you were a human. Do not break character.\""")
67
+ """
68
+ from edsl import Agent
69
+
70
+ if self.agent == Agent(): # if agent is empty, then return an empty prompt
71
+ return Prompt(text="")
72
+
73
+ return Prompt(text=self.agent.instruction)
74
+
75
+ @property
76
+ def agent_persona_prompt(self) -> Prompt:
77
+ """
78
+ >>> from edsl.agents.InvigilatorBase import InvigilatorBase
79
+ >>> i = InvigilatorBase.example()
80
+ >>> i.prompt_constructor.agent_persona_prompt
81
+ Prompt(text=\"""Your traits: {'age': 22, 'hair': 'brown', 'height': 5.5}\""")
82
+ """
83
+ from edsl import Agent
84
+
85
+ if self.agent == Agent(): # if agent is empty, then return an empty prompt
86
+ return Prompt(text="")
87
+
88
+ return self.agent.prompt()
89
+
90
+ def prior_answers_dict(self) -> dict:
91
+ d = self.survey.question_names_to_questions()
92
+ # This attaches the answer to the question
93
+ for question, answer in self.current_answers.items():
94
+ if question in d:
95
+ d[question].answer = answer
96
+ else:
97
+ # adds a comment to the question
98
+ if (new_question := question.split("_comment")[0]) in d:
99
+ d[new_question].comment = answer
100
+ return d
101
+
102
+ @property
103
+ def question_file_keys(self):
104
+ raw_question_text = self.question.question_text
105
+ variables = get_jinja2_variables(raw_question_text)
106
+ question_file_keys = []
107
+ for var in variables:
108
+ if var in self.scenario_file_keys:
109
+ question_file_keys.append(var)
110
+ return question_file_keys
111
+
112
+ @property
113
+ def question_instructions_prompt(self) -> Prompt:
114
+ """
115
+ >>> from edsl.agents.InvigilatorBase import InvigilatorBase
116
+ >>> i = InvigilatorBase.example()
117
+ >>> i.prompt_constructor.question_instructions_prompt
118
+ Prompt(text=\"""...
119
+ ...
120
+ """
121
+ # The user might have passed a custom prompt, which would be stored in _question_instructions_prompt
122
+ if not hasattr(self, "_question_instructions_prompt"):
123
+ # Gets the instructions for the question - this is how the question should be answered
124
+ question_prompt = self.question.get_instructions(model=self.model.model)
125
+
126
+ # Get the data for the question - this is a dictionary of the question data
127
+ # e.g., {'question_text': 'Do you like school?', 'question_name': 'q0', 'question_options': ['yes', 'no']}
128
+ question_data = self.question.data.copy()
129
+
130
+ # check to see if the question_options is actually a string
131
+ # This is used when the user is using the question_options as a variable from a scenario
132
+ # if "question_options" in question_data:
133
+ if isinstance(self.question.data.get("question_options", None), str):
134
+ env = Environment()
135
+ parsed_content = env.parse(self.question.data["question_options"])
136
+ question_option_key = list(
137
+ meta.find_undeclared_variables(parsed_content)
138
+ )[0]
139
+
140
+ # look to see if the question_option_key is in the scenario
141
+ if isinstance(
142
+ question_options := self.scenario.get(question_option_key), list
143
+ ):
144
+ question_data["question_options"] = question_options
145
+ self.question.question_options = question_options
146
+
147
+ # might be getting it from the prior answers
148
+ if self.prior_answers_dict().get(question_option_key) is not None:
149
+ prior_question = self.prior_answers_dict().get(question_option_key)
150
+ if hasattr(prior_question, "answer"):
151
+ if isinstance(prior_question.answer, list):
152
+ question_data["question_options"] = prior_question.answer
153
+ self.question.question_options = prior_question.answer
154
+ else:
155
+ placeholder_options = [
156
+ "N/A",
157
+ "Will be populated by prior answer",
158
+ "These are placeholder options",
159
+ ]
160
+ question_data["question_options"] = placeholder_options
161
+ self.question.question_options = placeholder_options
162
+
163
+ replacement_dict = (
164
+ {key: f"<see file {key}>" for key in self.scenario_file_keys}
165
+ | question_data
166
+ | {
167
+ k: v
168
+ for k, v in self.scenario.items()
169
+ if k not in self.scenario_file_keys
170
+ } # don't include images in the replacement dict
171
+ | self.prior_answers_dict()
172
+ | {"agent": self.agent}
173
+ | {
174
+ "use_code": getattr(self.question, "_use_code", True),
175
+ "include_comment": getattr(
176
+ self.question, "_include_comment", False
177
+ ),
178
+ }
179
+ )
180
+
181
+ rendered_instructions = question_prompt.render(replacement_dict)
182
+
183
+ # is there anything left to render?
184
+ undefined_template_variables = (
185
+ rendered_instructions.undefined_template_variables({})
186
+ )
187
+
188
+ # Check if it's the name of a question in the survey
189
+ for question_name in self.survey.question_names:
190
+ if question_name in undefined_template_variables:
191
+ print(
192
+ "Question name found in undefined_template_variables: ",
193
+ question_name,
194
+ )
195
+
196
+ if undefined_template_variables:
197
+ msg = f"Question instructions still has variables: {undefined_template_variables}."
198
+ import warnings
199
+
200
+ warnings.warn(msg)
201
+ # raise QuestionScenarioRenderError(
202
+ # f"Question instructions still has variables: {undefined_template_variables}."
203
+ # )
204
+
205
+ ####################################
206
+ # Check if question has instructions - these are instructions in a Survey that can apply to multiple follow-on questions
207
+ ####################################
208
+ relevant_instructions = self.survey.relevant_instructions(
209
+ self.question.question_name
210
+ )
211
+
212
+ if relevant_instructions != []:
213
+ # preamble_text = Prompt(
214
+ # text="You were given the following instructions: "
215
+ # )
216
+ preamble_text = Prompt(text="")
217
+ for instruction in relevant_instructions:
218
+ preamble_text += instruction.text
219
+ rendered_instructions = preamble_text + rendered_instructions
220
+
221
+ self._question_instructions_prompt = rendered_instructions
222
+ return self._question_instructions_prompt
223
+
224
+ @property
225
+ def prior_question_memory_prompt(self) -> Prompt:
226
+ if not hasattr(self, "_prior_question_memory_prompt"):
227
+ from edsl.prompts.Prompt import Prompt
228
+
229
+ memory_prompt = Prompt(text="")
230
+ if self.memory_plan is not None:
231
+ memory_prompt += self.create_memory_prompt(
232
+ self.question.question_name
233
+ ).render(self.scenario | self.prior_answers_dict())
234
+ self._prior_question_memory_prompt = memory_prompt
235
+ return self._prior_question_memory_prompt
236
+
237
+ def create_memory_prompt(self, question_name: str) -> Prompt:
238
+ """Create a memory for the agent.
239
+
240
+ The returns a memory prompt for the agent.
241
+
242
+ >>> from edsl.agents.InvigilatorBase import InvigilatorBase
243
+ >>> i = InvigilatorBase.example()
244
+ >>> i.current_answers = {"q0": "Prior answer"}
245
+ >>> i.memory_plan.add_single_memory("q1", "q0")
246
+ >>> p = i.prompt_constructor.create_memory_prompt("q1")
247
+ >>> p.text.strip().replace("\\n", " ").replace("\\t", " ")
248
+ 'Before the question you are now answering, you already answered the following question(s): Question: Do you like school? Answer: Prior answer'
249
+ """
250
+ return self.memory_plan.get_memory_prompt_fragment(
251
+ question_name, self.current_answers
252
+ )
253
+
254
+ def construct_system_prompt(self) -> Prompt:
255
+ """Construct the system prompt for the LLM call."""
256
+ import warnings
257
+
258
+ warnings.warn(
259
+ "This method is deprecated. Use get_prompts instead.", DeprecationWarning
260
+ )
261
+ return self.get_prompts()["system_prompt"]
262
+
263
+ def construct_user_prompt(self) -> Prompt:
264
+ """Construct the user prompt for the LLM call."""
265
+ import warnings
266
+
267
+ warnings.warn(
268
+ "This method is deprecated. Use get_prompts instead.", DeprecationWarning
269
+ )
270
+ return self.get_prompts()["user_prompt"]
271
+
272
+ def get_prompts(self) -> Dict[str, Prompt]:
273
+ """Get both prompts for the LLM call.
274
+
275
+ >>> from edsl import QuestionFreeText
276
+ >>> from edsl.agents.InvigilatorBase import InvigilatorBase
277
+ >>> q = QuestionFreeText(question_text="How are you today?", question_name="q_new")
278
+ >>> i = InvigilatorBase.example(question = q)
279
+ >>> i.get_prompts()
280
+ {'user_prompt': ..., 'system_prompt': ...}
281
+ """
282
+ # breakpoint()
283
+ prompts = self.prompt_plan.get_prompts(
284
+ agent_instructions=self.agent_instructions_prompt,
285
+ agent_persona=self.agent_persona_prompt,
286
+ question_instructions=self.question_instructions_prompt,
287
+ prior_question_memory=self.prior_question_memory_prompt,
288
+ )
289
+ if self.question_file_keys:
290
+ files_list = []
291
+ for key in self.question_file_keys:
292
+ files_list.append(self.scenario[key])
293
+ prompts["files_list"] = files_list
294
+ return prompts
295
+
296
+ def _get_scenario_with_image(self) -> Scenario:
297
+ """This is a helper function to get a scenario with an image, for testing purposes."""
298
+ from edsl import Scenario
299
+
300
+ try:
301
+ scenario = Scenario.from_image("../../static/logo.png")
302
+ except FileNotFoundError:
303
+ scenario = Scenario.from_image("static/logo.png")
304
+ return scenario
305
+
306
+
307
+ if __name__ == "__main__":
308
+ import doctest
309
+
310
+ doctest.testmod(optionflags=doctest.ELLIPSIS)