edsl 0.1.36.dev5__py3-none-any.whl → 0.1.36.dev6__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 +47 -47
  5. edsl/__version__.py +1 -1
  6. edsl/agents/Agent.py +804 -804
  7. edsl/agents/AgentList.py +337 -337
  8. edsl/agents/Invigilator.py +222 -222
  9. edsl/agents/InvigilatorBase.py +294 -294
  10. edsl/agents/PromptConstructor.py +312 -312
  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 +849 -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 +83 -83
  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 +72 -68
  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 -94
  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 +651 -651
  95. edsl/jobs/interviews/InterviewExceptionCollection.py +99 -99
  96. edsl/jobs/interviews/InterviewExceptionEntry.py +182 -182
  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 +337 -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 +358 -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 +433 -433
  186. edsl/results/Results.py +1158 -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 +443 -443
  196. edsl/scenarios/Scenario.py +507 -507
  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 +2 -2
  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.dev5.dist-info → edsl-0.1.36.dev6.dist-info}/LICENSE +21 -21
  254. {edsl-0.1.36.dev5.dist-info → edsl-0.1.36.dev6.dist-info}/METADATA +1 -1
  255. edsl-0.1.36.dev6.dist-info/RECORD +279 -0
  256. edsl-0.1.36.dev5.dist-info/RECORD +0 -279
  257. {edsl-0.1.36.dev5.dist-info → edsl-0.1.36.dev6.dist-info}/WHEEL +0 -0
@@ -1,312 +1,312 @@
1
- from __future__ import annotations
2
- from typing import Dict, Any, Optional, Set
3
- from collections import UserList
4
- import pdb
5
-
6
- from jinja2 import Environment, meta
7
-
8
- from edsl.prompts.Prompt import Prompt
9
- from edsl.data_transfer_models import ImageInfo
10
-
11
- # from edsl.prompts.registry import get_classes as prompt_lookup
12
- from edsl.exceptions import QuestionScenarioRenderError
13
-
14
- from edsl.agents.prompt_helpers import PromptComponent, PromptList, PromptPlan
15
-
16
-
17
- def get_jinja2_variables(template_str: str) -> Set[str]:
18
- """
19
- Extracts all variable names from a Jinja2 template using Jinja2's built-in parsing.
20
-
21
- Args:
22
- template_str (str): The Jinja2 template string
23
-
24
- Returns:
25
- Set[str]: A set of variable names found in the template
26
- """
27
- env = Environment()
28
- ast = env.parse(template_str)
29
- return meta.find_undeclared_variables(ast)
30
-
31
-
32
- class PromptConstructor:
33
- """
34
- The pieces of a prompt are:
35
- - The agent instructions - "You are answering questions as if you were a human. Do not break character."
36
- - The persona prompt - "You are an agent with the following persona: {'age': 22, 'hair': 'brown', 'height': 5.5}"
37
- - 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."
38
- - 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"
39
-
40
- This is mixed into the Invigilator class.
41
- """
42
-
43
- def __init__(self, invigilator):
44
- self.invigilator = invigilator
45
- self.agent = invigilator.agent
46
- self.question = invigilator.question
47
- self.scenario = invigilator.scenario
48
- self.survey = invigilator.survey
49
- self.model = invigilator.model
50
- self.current_answers = invigilator.current_answers
51
- self.memory_plan = invigilator.memory_plan
52
- self.prompt_plan = PromptPlan()
53
-
54
- @property
55
- def scenario_file_keys(self) -> list:
56
- """We need to find all the keys in the scenario that refer to FileStore objects.
57
- These will be used to append to the prompt a list of files that are part of the scenario.
58
- """
59
- from edsl.scenarios.FileStore import FileStore
60
-
61
- file_entries = []
62
- for key, value in self.scenario.items():
63
- if isinstance(value, FileStore):
64
- file_entries.append(key)
65
- return file_entries
66
-
67
- @property
68
- def agent_instructions_prompt(self) -> Prompt:
69
- """
70
- >>> from edsl.agents.InvigilatorBase import InvigilatorBase
71
- >>> i = InvigilatorBase.example()
72
- >>> i.prompt_constructor.agent_instructions_prompt
73
- Prompt(text=\"""You are answering questions as if you were a human. Do not break character.\""")
74
- """
75
- from edsl import Agent
76
-
77
- if self.agent == Agent(): # if agent is empty, then return an empty prompt
78
- return Prompt(text="")
79
-
80
- return Prompt(text=self.agent.instruction)
81
-
82
- @property
83
- def agent_persona_prompt(self) -> Prompt:
84
- """
85
- >>> from edsl.agents.InvigilatorBase import InvigilatorBase
86
- >>> i = InvigilatorBase.example()
87
- >>> i.prompt_constructor.agent_persona_prompt
88
- Prompt(text=\"""Your traits: {'age': 22, 'hair': 'brown', 'height': 5.5}\""")
89
- """
90
- from edsl import Agent
91
-
92
- if self.agent == Agent(): # if agent is empty, then return an empty prompt
93
- return Prompt(text="")
94
-
95
- return self.agent.prompt()
96
-
97
- def prior_answers_dict(self) -> dict:
98
- d = self.survey.question_names_to_questions()
99
- # This attaches the answer to the question
100
- for question, answer in self.current_answers.items():
101
- if question in d:
102
- d[question].answer = answer
103
- else:
104
- # adds a comment to the question
105
- if (new_question := question.split("_comment")[0]) in d:
106
- d[new_question].comment = answer
107
- return d
108
-
109
- @property
110
- def question_file_keys(self):
111
- raw_question_text = self.question.question_text
112
- variables = get_jinja2_variables(raw_question_text)
113
- question_file_keys = []
114
- for var in variables:
115
- if var in self.scenario_file_keys:
116
- question_file_keys.append(var)
117
- return question_file_keys
118
-
119
- @property
120
- def question_instructions_prompt(self) -> Prompt:
121
- """
122
- >>> from edsl.agents.InvigilatorBase import InvigilatorBase
123
- >>> i = InvigilatorBase.example()
124
- >>> i.prompt_constructor.question_instructions_prompt
125
- Prompt(text=\"""...
126
- ...
127
- """
128
- # The user might have passed a custom prompt, which would be stored in _question_instructions_prompt
129
- if not hasattr(self, "_question_instructions_prompt"):
130
- # Gets the instructions for the question - this is how the question should be answered
131
- question_prompt = self.question.get_instructions(model=self.model.model)
132
-
133
- # Get the data for the question - this is a dictionary of the question data
134
- # e.g., {'question_text': 'Do you like school?', 'question_name': 'q0', 'question_options': ['yes', 'no']}
135
- question_data = self.question.data.copy()
136
-
137
- # check to see if the question_options is actually a string
138
- # This is used when the user is using the question_options as a variable from a scenario
139
- # if "question_options" in question_data:
140
- if isinstance(self.question.data.get("question_options", None), str):
141
- env = Environment()
142
- parsed_content = env.parse(self.question.data["question_options"])
143
- question_option_key = list(
144
- meta.find_undeclared_variables(parsed_content)
145
- )[0]
146
-
147
- # look to see if the question_option_key is in the scenario
148
- if isinstance(
149
- question_options := self.scenario.get(question_option_key), list
150
- ):
151
- question_data["question_options"] = question_options
152
- self.question.question_options = question_options
153
-
154
- # might be getting it from the prior answers
155
- if self.prior_answers_dict().get(question_option_key) is not None:
156
- if isinstance(
157
- question_options := self.prior_answers_dict()
158
- .get(question_option_key)
159
- .answer,
160
- list,
161
- ):
162
- question_data["question_options"] = question_options
163
- self.question.question_options = question_options
164
-
165
- replacement_dict = (
166
- {key: f"<see file {key}>" for key in self.scenario_file_keys}
167
- | question_data
168
- | {
169
- k: v
170
- for k, v in self.scenario.items()
171
- if k not in self.scenario_file_keys
172
- } # don't include images in the replacement dict
173
- | self.prior_answers_dict()
174
- | {"agent": self.agent}
175
- | {
176
- "use_code": getattr(self.question, "_use_code", True),
177
- "include_comment": getattr(
178
- self.question, "_include_comment", False
179
- ),
180
- }
181
- )
182
-
183
- rendered_instructions = question_prompt.render(replacement_dict)
184
-
185
- # is there anything left to render?
186
- undefined_template_variables = (
187
- rendered_instructions.undefined_template_variables({})
188
- )
189
-
190
- # Check if it's the name of a question in the survey
191
- for question_name in self.survey.question_names:
192
- if question_name in undefined_template_variables:
193
- print(
194
- "Question name found in undefined_template_variables: ",
195
- question_name,
196
- )
197
-
198
- if undefined_template_variables:
199
- msg = f"Question instructions still has variables: {undefined_template_variables}."
200
- import warnings
201
-
202
- warnings.warn(msg)
203
- # raise QuestionScenarioRenderError(
204
- # f"Question instructions still has variables: {undefined_template_variables}."
205
- # )
206
-
207
- ####################################
208
- # Check if question has instructions - these are instructions in a Survey that can apply to multiple follow-on questions
209
- ####################################
210
- relevant_instructions = self.survey.relevant_instructions(
211
- self.question.question_name
212
- )
213
-
214
- if relevant_instructions != []:
215
- # preamble_text = Prompt(
216
- # text="You were given the following instructions: "
217
- # )
218
- preamble_text = Prompt(text="")
219
- for instruction in relevant_instructions:
220
- preamble_text += instruction.text
221
- rendered_instructions = preamble_text + rendered_instructions
222
-
223
- self._question_instructions_prompt = rendered_instructions
224
- return self._question_instructions_prompt
225
-
226
- @property
227
- def prior_question_memory_prompt(self) -> Prompt:
228
- if not hasattr(self, "_prior_question_memory_prompt"):
229
- from edsl.prompts.Prompt import Prompt
230
-
231
- memory_prompt = Prompt(text="")
232
- if self.memory_plan is not None:
233
- memory_prompt += self.create_memory_prompt(
234
- self.question.question_name
235
- ).render(self.scenario | self.prior_answers_dict())
236
- self._prior_question_memory_prompt = memory_prompt
237
- return self._prior_question_memory_prompt
238
-
239
- def create_memory_prompt(self, question_name: str) -> Prompt:
240
- """Create a memory for the agent.
241
-
242
- The returns a memory prompt for the agent.
243
-
244
- >>> from edsl.agents.InvigilatorBase import InvigilatorBase
245
- >>> i = InvigilatorBase.example()
246
- >>> i.current_answers = {"q0": "Prior answer"}
247
- >>> i.memory_plan.add_single_memory("q1", "q0")
248
- >>> p = i.prompt_constructor.create_memory_prompt("q1")
249
- >>> p.text.strip().replace("\\n", " ").replace("\\t", " ")
250
- 'Before the question you are now answering, you already answered the following question(s): Question: Do you like school? Answer: Prior answer'
251
- """
252
- return self.memory_plan.get_memory_prompt_fragment(
253
- question_name, self.current_answers
254
- )
255
-
256
- def construct_system_prompt(self) -> Prompt:
257
- """Construct the system prompt for the LLM call."""
258
- import warnings
259
-
260
- warnings.warn(
261
- "This method is deprecated. Use get_prompts instead.", DeprecationWarning
262
- )
263
- return self.get_prompts()["system_prompt"]
264
-
265
- def construct_user_prompt(self) -> Prompt:
266
- """Construct the user prompt for the LLM call."""
267
- import warnings
268
-
269
- warnings.warn(
270
- "This method is deprecated. Use get_prompts instead.", DeprecationWarning
271
- )
272
- return self.get_prompts()["user_prompt"]
273
-
274
- def get_prompts(self) -> Dict[str, Prompt]:
275
- """Get both prompts for the LLM call.
276
-
277
- >>> from edsl import QuestionFreeText
278
- >>> from edsl.agents.InvigilatorBase import InvigilatorBase
279
- >>> q = QuestionFreeText(question_text="How are you today?", question_name="q_new")
280
- >>> i = InvigilatorBase.example(question = q)
281
- >>> i.get_prompts()
282
- {'user_prompt': ..., 'system_prompt': ...}
283
- """
284
- # breakpoint()
285
- prompts = self.prompt_plan.get_prompts(
286
- agent_instructions=self.agent_instructions_prompt,
287
- agent_persona=self.agent_persona_prompt,
288
- question_instructions=self.question_instructions_prompt,
289
- prior_question_memory=self.prior_question_memory_prompt,
290
- )
291
- if self.question_file_keys:
292
- files_list = []
293
- for key in self.question_file_keys:
294
- files_list.append(self.scenario[key])
295
- prompts["files_list"] = files_list
296
- return prompts
297
-
298
- def _get_scenario_with_image(self) -> Scenario:
299
- """This is a helper function to get a scenario with an image, for testing purposes."""
300
- from edsl import Scenario
301
-
302
- try:
303
- scenario = Scenario.from_image("../../static/logo.png")
304
- except FileNotFoundError:
305
- scenario = Scenario.from_image("static/logo.png")
306
- return scenario
307
-
308
-
309
- if __name__ == "__main__":
310
- import doctest
311
-
312
- doctest.testmod(optionflags=doctest.ELLIPSIS)
1
+ from __future__ import annotations
2
+ from typing import Dict, Any, Optional, Set
3
+ from collections import UserList
4
+ import pdb
5
+
6
+ from jinja2 import Environment, meta
7
+
8
+ from edsl.prompts.Prompt import Prompt
9
+ from edsl.data_transfer_models import ImageInfo
10
+
11
+ # from edsl.prompts.registry import get_classes as prompt_lookup
12
+ from edsl.exceptions import QuestionScenarioRenderError
13
+
14
+ from edsl.agents.prompt_helpers import PromptComponent, PromptList, PromptPlan
15
+
16
+
17
+ def get_jinja2_variables(template_str: str) -> Set[str]:
18
+ """
19
+ Extracts all variable names from a Jinja2 template using Jinja2's built-in parsing.
20
+
21
+ Args:
22
+ template_str (str): The Jinja2 template string
23
+
24
+ Returns:
25
+ Set[str]: A set of variable names found in the template
26
+ """
27
+ env = Environment()
28
+ ast = env.parse(template_str)
29
+ return meta.find_undeclared_variables(ast)
30
+
31
+
32
+ class PromptConstructor:
33
+ """
34
+ The pieces of a prompt are:
35
+ - The agent instructions - "You are answering questions as if you were a human. Do not break character."
36
+ - The persona prompt - "You are an agent with the following persona: {'age': 22, 'hair': 'brown', 'height': 5.5}"
37
+ - 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."
38
+ - 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"
39
+
40
+ This is mixed into the Invigilator class.
41
+ """
42
+
43
+ def __init__(self, invigilator):
44
+ self.invigilator = invigilator
45
+ self.agent = invigilator.agent
46
+ self.question = invigilator.question
47
+ self.scenario = invigilator.scenario
48
+ self.survey = invigilator.survey
49
+ self.model = invigilator.model
50
+ self.current_answers = invigilator.current_answers
51
+ self.memory_plan = invigilator.memory_plan
52
+ self.prompt_plan = PromptPlan()
53
+
54
+ @property
55
+ def scenario_file_keys(self) -> list:
56
+ """We need to find all the keys in the scenario that refer to FileStore objects.
57
+ These will be used to append to the prompt a list of files that are part of the scenario.
58
+ """
59
+ from edsl.scenarios.FileStore import FileStore
60
+
61
+ file_entries = []
62
+ for key, value in self.scenario.items():
63
+ if isinstance(value, FileStore):
64
+ file_entries.append(key)
65
+ return file_entries
66
+
67
+ @property
68
+ def agent_instructions_prompt(self) -> Prompt:
69
+ """
70
+ >>> from edsl.agents.InvigilatorBase import InvigilatorBase
71
+ >>> i = InvigilatorBase.example()
72
+ >>> i.prompt_constructor.agent_instructions_prompt
73
+ Prompt(text=\"""You are answering questions as if you were a human. Do not break character.\""")
74
+ """
75
+ from edsl import Agent
76
+
77
+ if self.agent == Agent(): # if agent is empty, then return an empty prompt
78
+ return Prompt(text="")
79
+
80
+ return Prompt(text=self.agent.instruction)
81
+
82
+ @property
83
+ def agent_persona_prompt(self) -> Prompt:
84
+ """
85
+ >>> from edsl.agents.InvigilatorBase import InvigilatorBase
86
+ >>> i = InvigilatorBase.example()
87
+ >>> i.prompt_constructor.agent_persona_prompt
88
+ Prompt(text=\"""Your traits: {'age': 22, 'hair': 'brown', 'height': 5.5}\""")
89
+ """
90
+ from edsl import Agent
91
+
92
+ if self.agent == Agent(): # if agent is empty, then return an empty prompt
93
+ return Prompt(text="")
94
+
95
+ return self.agent.prompt()
96
+
97
+ def prior_answers_dict(self) -> dict:
98
+ d = self.survey.question_names_to_questions()
99
+ # This attaches the answer to the question
100
+ for question, answer in self.current_answers.items():
101
+ if question in d:
102
+ d[question].answer = answer
103
+ else:
104
+ # adds a comment to the question
105
+ if (new_question := question.split("_comment")[0]) in d:
106
+ d[new_question].comment = answer
107
+ return d
108
+
109
+ @property
110
+ def question_file_keys(self):
111
+ raw_question_text = self.question.question_text
112
+ variables = get_jinja2_variables(raw_question_text)
113
+ question_file_keys = []
114
+ for var in variables:
115
+ if var in self.scenario_file_keys:
116
+ question_file_keys.append(var)
117
+ return question_file_keys
118
+
119
+ @property
120
+ def question_instructions_prompt(self) -> Prompt:
121
+ """
122
+ >>> from edsl.agents.InvigilatorBase import InvigilatorBase
123
+ >>> i = InvigilatorBase.example()
124
+ >>> i.prompt_constructor.question_instructions_prompt
125
+ Prompt(text=\"""...
126
+ ...
127
+ """
128
+ # The user might have passed a custom prompt, which would be stored in _question_instructions_prompt
129
+ if not hasattr(self, "_question_instructions_prompt"):
130
+ # Gets the instructions for the question - this is how the question should be answered
131
+ question_prompt = self.question.get_instructions(model=self.model.model)
132
+
133
+ # Get the data for the question - this is a dictionary of the question data
134
+ # e.g., {'question_text': 'Do you like school?', 'question_name': 'q0', 'question_options': ['yes', 'no']}
135
+ question_data = self.question.data.copy()
136
+
137
+ # check to see if the question_options is actually a string
138
+ # This is used when the user is using the question_options as a variable from a scenario
139
+ # if "question_options" in question_data:
140
+ if isinstance(self.question.data.get("question_options", None), str):
141
+ env = Environment()
142
+ parsed_content = env.parse(self.question.data["question_options"])
143
+ question_option_key = list(
144
+ meta.find_undeclared_variables(parsed_content)
145
+ )[0]
146
+
147
+ # look to see if the question_option_key is in the scenario
148
+ if isinstance(
149
+ question_options := self.scenario.get(question_option_key), list
150
+ ):
151
+ question_data["question_options"] = question_options
152
+ self.question.question_options = question_options
153
+
154
+ # might be getting it from the prior answers
155
+ if self.prior_answers_dict().get(question_option_key) is not None:
156
+ if isinstance(
157
+ question_options := self.prior_answers_dict()
158
+ .get(question_option_key)
159
+ .answer,
160
+ list,
161
+ ):
162
+ question_data["question_options"] = question_options
163
+ self.question.question_options = question_options
164
+
165
+ replacement_dict = (
166
+ {key: f"<see file {key}>" for key in self.scenario_file_keys}
167
+ | question_data
168
+ | {
169
+ k: v
170
+ for k, v in self.scenario.items()
171
+ if k not in self.scenario_file_keys
172
+ } # don't include images in the replacement dict
173
+ | self.prior_answers_dict()
174
+ | {"agent": self.agent}
175
+ | {
176
+ "use_code": getattr(self.question, "_use_code", True),
177
+ "include_comment": getattr(
178
+ self.question, "_include_comment", False
179
+ ),
180
+ }
181
+ )
182
+
183
+ rendered_instructions = question_prompt.render(replacement_dict)
184
+
185
+ # is there anything left to render?
186
+ undefined_template_variables = (
187
+ rendered_instructions.undefined_template_variables({})
188
+ )
189
+
190
+ # Check if it's the name of a question in the survey
191
+ for question_name in self.survey.question_names:
192
+ if question_name in undefined_template_variables:
193
+ print(
194
+ "Question name found in undefined_template_variables: ",
195
+ question_name,
196
+ )
197
+
198
+ if undefined_template_variables:
199
+ msg = f"Question instructions still has variables: {undefined_template_variables}."
200
+ import warnings
201
+
202
+ warnings.warn(msg)
203
+ # raise QuestionScenarioRenderError(
204
+ # f"Question instructions still has variables: {undefined_template_variables}."
205
+ # )
206
+
207
+ ####################################
208
+ # Check if question has instructions - these are instructions in a Survey that can apply to multiple follow-on questions
209
+ ####################################
210
+ relevant_instructions = self.survey.relevant_instructions(
211
+ self.question.question_name
212
+ )
213
+
214
+ if relevant_instructions != []:
215
+ # preamble_text = Prompt(
216
+ # text="You were given the following instructions: "
217
+ # )
218
+ preamble_text = Prompt(text="")
219
+ for instruction in relevant_instructions:
220
+ preamble_text += instruction.text
221
+ rendered_instructions = preamble_text + rendered_instructions
222
+
223
+ self._question_instructions_prompt = rendered_instructions
224
+ return self._question_instructions_prompt
225
+
226
+ @property
227
+ def prior_question_memory_prompt(self) -> Prompt:
228
+ if not hasattr(self, "_prior_question_memory_prompt"):
229
+ from edsl.prompts.Prompt import Prompt
230
+
231
+ memory_prompt = Prompt(text="")
232
+ if self.memory_plan is not None:
233
+ memory_prompt += self.create_memory_prompt(
234
+ self.question.question_name
235
+ ).render(self.scenario | self.prior_answers_dict())
236
+ self._prior_question_memory_prompt = memory_prompt
237
+ return self._prior_question_memory_prompt
238
+
239
+ def create_memory_prompt(self, question_name: str) -> Prompt:
240
+ """Create a memory for the agent.
241
+
242
+ The returns a memory prompt for the agent.
243
+
244
+ >>> from edsl.agents.InvigilatorBase import InvigilatorBase
245
+ >>> i = InvigilatorBase.example()
246
+ >>> i.current_answers = {"q0": "Prior answer"}
247
+ >>> i.memory_plan.add_single_memory("q1", "q0")
248
+ >>> p = i.prompt_constructor.create_memory_prompt("q1")
249
+ >>> p.text.strip().replace("\\n", " ").replace("\\t", " ")
250
+ 'Before the question you are now answering, you already answered the following question(s): Question: Do you like school? Answer: Prior answer'
251
+ """
252
+ return self.memory_plan.get_memory_prompt_fragment(
253
+ question_name, self.current_answers
254
+ )
255
+
256
+ def construct_system_prompt(self) -> Prompt:
257
+ """Construct the system prompt for the LLM call."""
258
+ import warnings
259
+
260
+ warnings.warn(
261
+ "This method is deprecated. Use get_prompts instead.", DeprecationWarning
262
+ )
263
+ return self.get_prompts()["system_prompt"]
264
+
265
+ def construct_user_prompt(self) -> Prompt:
266
+ """Construct the user prompt for the LLM call."""
267
+ import warnings
268
+
269
+ warnings.warn(
270
+ "This method is deprecated. Use get_prompts instead.", DeprecationWarning
271
+ )
272
+ return self.get_prompts()["user_prompt"]
273
+
274
+ def get_prompts(self) -> Dict[str, Prompt]:
275
+ """Get both prompts for the LLM call.
276
+
277
+ >>> from edsl import QuestionFreeText
278
+ >>> from edsl.agents.InvigilatorBase import InvigilatorBase
279
+ >>> q = QuestionFreeText(question_text="How are you today?", question_name="q_new")
280
+ >>> i = InvigilatorBase.example(question = q)
281
+ >>> i.get_prompts()
282
+ {'user_prompt': ..., 'system_prompt': ...}
283
+ """
284
+ # breakpoint()
285
+ prompts = self.prompt_plan.get_prompts(
286
+ agent_instructions=self.agent_instructions_prompt,
287
+ agent_persona=self.agent_persona_prompt,
288
+ question_instructions=self.question_instructions_prompt,
289
+ prior_question_memory=self.prior_question_memory_prompt,
290
+ )
291
+ if self.question_file_keys:
292
+ files_list = []
293
+ for key in self.question_file_keys:
294
+ files_list.append(self.scenario[key])
295
+ prompts["files_list"] = files_list
296
+ return prompts
297
+
298
+ def _get_scenario_with_image(self) -> Scenario:
299
+ """This is a helper function to get a scenario with an image, for testing purposes."""
300
+ from edsl import Scenario
301
+
302
+ try:
303
+ scenario = Scenario.from_image("../../static/logo.png")
304
+ except FileNotFoundError:
305
+ scenario = Scenario.from_image("static/logo.png")
306
+ return scenario
307
+
308
+
309
+ if __name__ == "__main__":
310
+ import doctest
311
+
312
+ doctest.testmod(optionflags=doctest.ELLIPSIS)
edsl/agents/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
- from edsl.agents.Agent import Agent
2
- from edsl.agents.AgentList import AgentList
3
- from edsl.agents.InvigilatorBase import InvigilatorBase
1
+ from edsl.agents.Agent import Agent
2
+ from edsl.agents.AgentList import AgentList
3
+ from edsl.agents.InvigilatorBase import InvigilatorBase