edsl 0.1.33__py3-none-any.whl → 0.1.33.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 (180) hide show
  1. edsl/Base.py +3 -9
  2. edsl/__init__.py +3 -8
  3. edsl/__version__.py +1 -1
  4. edsl/agents/Agent.py +8 -40
  5. edsl/agents/AgentList.py +0 -43
  6. edsl/agents/Invigilator.py +219 -135
  7. edsl/agents/InvigilatorBase.py +59 -148
  8. edsl/agents/{PromptConstructor.py → PromptConstructionMixin.py} +89 -138
  9. edsl/agents/__init__.py +0 -1
  10. edsl/config.py +56 -47
  11. edsl/coop/coop.py +7 -50
  12. edsl/data/Cache.py +1 -35
  13. edsl/data_transfer_models.py +38 -73
  14. edsl/enums.py +0 -4
  15. edsl/exceptions/language_models.py +1 -25
  16. edsl/exceptions/questions.py +5 -62
  17. edsl/exceptions/results.py +0 -4
  18. edsl/inference_services/AnthropicService.py +11 -13
  19. edsl/inference_services/AwsBedrock.py +17 -19
  20. edsl/inference_services/AzureAI.py +20 -37
  21. edsl/inference_services/GoogleService.py +12 -16
  22. edsl/inference_services/GroqService.py +0 -2
  23. edsl/inference_services/InferenceServiceABC.py +3 -58
  24. edsl/inference_services/OpenAIService.py +54 -48
  25. edsl/inference_services/models_available_cache.py +6 -0
  26. edsl/inference_services/registry.py +0 -6
  27. edsl/jobs/Answers.py +12 -10
  28. edsl/jobs/Jobs.py +21 -36
  29. edsl/jobs/buckets/BucketCollection.py +15 -24
  30. edsl/jobs/buckets/TokenBucket.py +14 -93
  31. edsl/jobs/interviews/Interview.py +78 -366
  32. edsl/jobs/interviews/InterviewExceptionEntry.py +19 -85
  33. edsl/jobs/interviews/InterviewTaskBuildingMixin.py +286 -0
  34. edsl/jobs/interviews/{InterviewExceptionCollection.py → interview_exception_tracking.py} +68 -14
  35. edsl/jobs/interviews/retry_management.py +37 -0
  36. edsl/jobs/runners/JobsRunnerAsyncio.py +175 -146
  37. edsl/jobs/runners/JobsRunnerStatusMixin.py +333 -0
  38. edsl/jobs/tasks/QuestionTaskCreator.py +23 -30
  39. edsl/jobs/tasks/TaskHistory.py +213 -148
  40. edsl/language_models/LanguageModel.py +156 -261
  41. edsl/language_models/ModelList.py +2 -2
  42. edsl/language_models/RegisterLanguageModelsMeta.py +29 -14
  43. edsl/language_models/registry.py +6 -23
  44. edsl/language_models/repair.py +19 -0
  45. edsl/prompts/Prompt.py +2 -52
  46. edsl/questions/AnswerValidatorMixin.py +26 -23
  47. edsl/questions/QuestionBase.py +249 -329
  48. edsl/questions/QuestionBudget.py +41 -99
  49. edsl/questions/QuestionCheckBox.py +35 -227
  50. edsl/questions/QuestionExtract.py +27 -98
  51. edsl/questions/QuestionFreeText.py +29 -52
  52. edsl/questions/QuestionFunctional.py +0 -7
  53. edsl/questions/QuestionList.py +22 -141
  54. edsl/questions/QuestionMultipleChoice.py +65 -159
  55. edsl/questions/QuestionNumerical.py +46 -88
  56. edsl/questions/QuestionRank.py +24 -182
  57. edsl/questions/RegisterQuestionsMeta.py +12 -31
  58. edsl/questions/__init__.py +4 -3
  59. edsl/questions/derived/QuestionLikertFive.py +5 -10
  60. edsl/questions/derived/QuestionLinearScale.py +2 -15
  61. edsl/questions/derived/QuestionTopK.py +1 -10
  62. edsl/questions/derived/QuestionYesNo.py +3 -24
  63. edsl/questions/descriptors.py +7 -43
  64. edsl/questions/question_registry.py +2 -6
  65. edsl/results/Dataset.py +0 -20
  66. edsl/results/DatasetExportMixin.py +48 -46
  67. edsl/results/Result.py +5 -32
  68. edsl/results/Results.py +46 -135
  69. edsl/results/ResultsDBMixin.py +3 -3
  70. edsl/scenarios/FileStore.py +10 -71
  71. edsl/scenarios/Scenario.py +25 -96
  72. edsl/scenarios/ScenarioImageMixin.py +2 -2
  73. edsl/scenarios/ScenarioList.py +39 -361
  74. edsl/scenarios/ScenarioListExportMixin.py +0 -9
  75. edsl/scenarios/ScenarioListPdfMixin.py +4 -150
  76. edsl/study/SnapShot.py +1 -8
  77. edsl/study/Study.py +0 -32
  78. edsl/surveys/Rule.py +1 -10
  79. edsl/surveys/RuleCollection.py +5 -21
  80. edsl/surveys/Survey.py +310 -636
  81. edsl/surveys/SurveyExportMixin.py +9 -71
  82. edsl/surveys/SurveyFlowVisualizationMixin.py +1 -2
  83. edsl/surveys/SurveyQualtricsImport.py +4 -75
  84. edsl/utilities/gcp_bucket/simple_example.py +9 -0
  85. edsl/utilities/utilities.py +1 -9
  86. {edsl-0.1.33.dist-info → edsl-0.1.33.dev1.dist-info}/METADATA +2 -5
  87. edsl-0.1.33.dev1.dist-info/RECORD +209 -0
  88. edsl/TemplateLoader.py +0 -24
  89. edsl/auto/AutoStudy.py +0 -117
  90. edsl/auto/StageBase.py +0 -230
  91. edsl/auto/StageGenerateSurvey.py +0 -178
  92. edsl/auto/StageLabelQuestions.py +0 -125
  93. edsl/auto/StagePersona.py +0 -61
  94. edsl/auto/StagePersonaDimensionValueRanges.py +0 -88
  95. edsl/auto/StagePersonaDimensionValues.py +0 -74
  96. edsl/auto/StagePersonaDimensions.py +0 -69
  97. edsl/auto/StageQuestions.py +0 -73
  98. edsl/auto/SurveyCreatorPipeline.py +0 -21
  99. edsl/auto/utilities.py +0 -224
  100. edsl/coop/PriceFetcher.py +0 -58
  101. edsl/inference_services/MistralAIService.py +0 -120
  102. edsl/inference_services/TestService.py +0 -80
  103. edsl/inference_services/TogetherAIService.py +0 -170
  104. edsl/jobs/FailedQuestion.py +0 -78
  105. edsl/jobs/runners/JobsRunnerStatus.py +0 -331
  106. edsl/language_models/fake_openai_call.py +0 -15
  107. edsl/language_models/fake_openai_service.py +0 -61
  108. edsl/language_models/utilities.py +0 -61
  109. edsl/questions/QuestionBaseGenMixin.py +0 -133
  110. edsl/questions/QuestionBasePromptsMixin.py +0 -266
  111. edsl/questions/Quick.py +0 -41
  112. edsl/questions/ResponseValidatorABC.py +0 -170
  113. edsl/questions/decorators.py +0 -21
  114. edsl/questions/prompt_templates/question_budget.jinja +0 -13
  115. edsl/questions/prompt_templates/question_checkbox.jinja +0 -32
  116. edsl/questions/prompt_templates/question_extract.jinja +0 -11
  117. edsl/questions/prompt_templates/question_free_text.jinja +0 -3
  118. edsl/questions/prompt_templates/question_linear_scale.jinja +0 -11
  119. edsl/questions/prompt_templates/question_list.jinja +0 -17
  120. edsl/questions/prompt_templates/question_multiple_choice.jinja +0 -33
  121. edsl/questions/prompt_templates/question_numerical.jinja +0 -37
  122. edsl/questions/templates/__init__.py +0 -0
  123. edsl/questions/templates/budget/__init__.py +0 -0
  124. edsl/questions/templates/budget/answering_instructions.jinja +0 -7
  125. edsl/questions/templates/budget/question_presentation.jinja +0 -7
  126. edsl/questions/templates/checkbox/__init__.py +0 -0
  127. edsl/questions/templates/checkbox/answering_instructions.jinja +0 -10
  128. edsl/questions/templates/checkbox/question_presentation.jinja +0 -22
  129. edsl/questions/templates/extract/__init__.py +0 -0
  130. edsl/questions/templates/extract/answering_instructions.jinja +0 -7
  131. edsl/questions/templates/extract/question_presentation.jinja +0 -1
  132. edsl/questions/templates/free_text/__init__.py +0 -0
  133. edsl/questions/templates/free_text/answering_instructions.jinja +0 -0
  134. edsl/questions/templates/free_text/question_presentation.jinja +0 -1
  135. edsl/questions/templates/likert_five/__init__.py +0 -0
  136. edsl/questions/templates/likert_five/answering_instructions.jinja +0 -10
  137. edsl/questions/templates/likert_five/question_presentation.jinja +0 -12
  138. edsl/questions/templates/linear_scale/__init__.py +0 -0
  139. edsl/questions/templates/linear_scale/answering_instructions.jinja +0 -5
  140. edsl/questions/templates/linear_scale/question_presentation.jinja +0 -5
  141. edsl/questions/templates/list/__init__.py +0 -0
  142. edsl/questions/templates/list/answering_instructions.jinja +0 -4
  143. edsl/questions/templates/list/question_presentation.jinja +0 -5
  144. edsl/questions/templates/multiple_choice/__init__.py +0 -0
  145. edsl/questions/templates/multiple_choice/answering_instructions.jinja +0 -9
  146. edsl/questions/templates/multiple_choice/html.jinja +0 -0
  147. edsl/questions/templates/multiple_choice/question_presentation.jinja +0 -12
  148. edsl/questions/templates/numerical/__init__.py +0 -0
  149. edsl/questions/templates/numerical/answering_instructions.jinja +0 -8
  150. edsl/questions/templates/numerical/question_presentation.jinja +0 -7
  151. edsl/questions/templates/rank/__init__.py +0 -0
  152. edsl/questions/templates/rank/answering_instructions.jinja +0 -11
  153. edsl/questions/templates/rank/question_presentation.jinja +0 -15
  154. edsl/questions/templates/top_k/__init__.py +0 -0
  155. edsl/questions/templates/top_k/answering_instructions.jinja +0 -8
  156. edsl/questions/templates/top_k/question_presentation.jinja +0 -22
  157. edsl/questions/templates/yes_no/__init__.py +0 -0
  158. edsl/questions/templates/yes_no/answering_instructions.jinja +0 -6
  159. edsl/questions/templates/yes_no/question_presentation.jinja +0 -12
  160. edsl/results/DatasetTree.py +0 -145
  161. edsl/results/Selector.py +0 -118
  162. edsl/results/tree_explore.py +0 -115
  163. edsl/surveys/instructions/ChangeInstruction.py +0 -47
  164. edsl/surveys/instructions/Instruction.py +0 -34
  165. edsl/surveys/instructions/InstructionCollection.py +0 -77
  166. edsl/surveys/instructions/__init__.py +0 -0
  167. edsl/templates/error_reporting/base.html +0 -24
  168. edsl/templates/error_reporting/exceptions_by_model.html +0 -35
  169. edsl/templates/error_reporting/exceptions_by_question_name.html +0 -17
  170. edsl/templates/error_reporting/exceptions_by_type.html +0 -17
  171. edsl/templates/error_reporting/interview_details.html +0 -116
  172. edsl/templates/error_reporting/interviews.html +0 -10
  173. edsl/templates/error_reporting/overview.html +0 -5
  174. edsl/templates/error_reporting/performance_plot.html +0 -2
  175. edsl/templates/error_reporting/report.css +0 -74
  176. edsl/templates/error_reporting/report.html +0 -118
  177. edsl/templates/error_reporting/report.js +0 -25
  178. edsl-0.1.33.dist-info/RECORD +0 -295
  179. {edsl-0.1.33.dist-info → edsl-0.1.33.dev1.dist-info}/LICENSE +0 -0
  180. {edsl-0.1.33.dist-info → edsl-0.1.33.dev1.dist-info}/WHEEL +0 -0
@@ -8,27 +8,34 @@ from edsl.data_transfer_models import AgentResponseDict
8
8
 
9
9
  from edsl.data.Cache import Cache
10
10
 
11
+ # from edsl.agents.Agent import Agent
11
12
  from edsl.questions.QuestionBase import QuestionBase
12
13
  from edsl.scenarios.Scenario import Scenario
13
14
  from edsl.surveys.MemoryPlan import MemoryPlan
14
15
  from edsl.language_models.LanguageModel import LanguageModel
15
16
 
16
- from edsl.data_transfer_models import EDSLResultObjectInput
17
- from edsl.agents.PromptConstructor import PromptConstructor
18
-
19
17
 
20
18
  class InvigilatorBase(ABC):
21
19
  """An invigiator (someone who administers an exam) is a class that is responsible for administering a question to an agent.
22
20
 
23
21
  >>> InvigilatorBase.example().answer_question()
24
- {'message': [{'text': 'SPAM!'}], 'usage': {'prompt_tokens': 1, 'completion_tokens': 1}}
22
+ {'message': '{"answer": "SPAM!"}'}
25
23
 
26
- >>> InvigilatorBase.example().get_failed_task_result(failure_reason="Failed to get response").comment
27
- 'Failed to get response'
24
+ >>> InvigilatorBase.example().get_failed_task_result()
25
+ {'answer': None, 'comment': 'Failed to get response', ...
28
26
 
29
27
  This returns an empty prompt because there is no memory the agent needs to have at q0.
30
28
 
29
+ >>> InvigilatorBase.example().create_memory_prompt("q0")
30
+ Prompt(text=\"""\""")
31
31
 
32
+ >>> i = InvigilatorBase.example()
33
+ >>> i.current_answers = {"q0": "Prior answer"}
34
+ >>> i.memory_plan.add_single_memory("q1", "q0")
35
+ >>> i.create_memory_prompt("q1")
36
+ Prompt(text=\"""
37
+ Before the question you are now answering, you already answered the following question(s):
38
+ ...
32
39
  """
33
40
 
34
41
  def __init__(
@@ -44,7 +51,6 @@ class InvigilatorBase(ABC):
44
51
  iteration: Optional[int] = 1,
45
52
  additional_prompt_data: Optional[dict] = None,
46
53
  sidecar_model: Optional[LanguageModel] = None,
47
- raise_validation_errors: Optional[bool] = True,
48
54
  ):
49
55
  """Initialize a new Invigilator."""
50
56
  self.agent = agent
@@ -58,78 +64,6 @@ class InvigilatorBase(ABC):
58
64
  self.cache = cache
59
65
  self.sidecar_model = sidecar_model
60
66
  self.survey = survey
61
- self.raise_validation_errors = raise_validation_errors
62
-
63
- self.raw_model_response = (
64
- None # placeholder for the raw response from the model
65
- )
66
-
67
- @property
68
- def prompt_constructor(self) -> PromptConstructor:
69
- """Return the prompt constructor."""
70
- return PromptConstructor(self)
71
-
72
- def to_dict(self):
73
- attributes = [
74
- "agent",
75
- "question",
76
- "scenario",
77
- "model",
78
- "memory_plan",
79
- "current_answers",
80
- "iteration",
81
- "additional_prompt_data",
82
- "cache",
83
- "sidecar_model",
84
- "survey",
85
- ]
86
-
87
- def serialize_attribute(attr):
88
- value = getattr(self, attr)
89
- if value is None:
90
- return None
91
- if hasattr(value, "to_dict"):
92
- return value.to_dict()
93
- if isinstance(value, (int, float, str, bool, dict, list)):
94
- return value
95
- return str(value)
96
-
97
- return {attr: serialize_attribute(attr) for attr in attributes}
98
-
99
- @classmethod
100
- def from_dict(cls, data):
101
- from edsl.agents.Agent import Agent
102
- from edsl.questions import QuestionBase
103
- from edsl.scenarios.Scenario import Scenario
104
- from edsl.surveys.MemoryPlan import MemoryPlan
105
- from edsl.language_models.LanguageModel import LanguageModel
106
- from edsl.surveys.Survey import Survey
107
-
108
- agent = Agent.from_dict(data["agent"])
109
- question = QuestionBase.from_dict(data["question"])
110
- scenario = Scenario.from_dict(data["scenario"])
111
- model = LanguageModel.from_dict(data["model"])
112
- memory_plan = MemoryPlan.from_dict(data["memory_plan"])
113
- survey = Survey.from_dict(data["survey"])
114
- current_answers = data["current_answers"]
115
- iteration = data["iteration"]
116
- additional_prompt_data = data["additional_prompt_data"]
117
- cache = Cache.from_dict(data["cache"])
118
- sidecar_model = LanguageModel.from_dict(data["sidecar_model"])
119
-
120
- return cls(
121
- agent=agent,
122
- question=question,
123
- scenario=scenario,
124
- model=model,
125
- memory_plan=memory_plan,
126
- current_answers=current_answers,
127
- survey=survey,
128
- iteration=iteration,
129
- additional_prompt_data=additional_prompt_data,
130
- cache=cache,
131
- sidecar_model=sidecar_model,
132
- )
133
67
 
134
68
  def __repr__(self) -> str:
135
69
  """Return a string representation of the Invigilator.
@@ -140,45 +74,18 @@ class InvigilatorBase(ABC):
140
74
  """
141
75
  return f"{self.__class__.__name__}(agent={repr(self.agent)}, question={repr(self.question)}, scneario={repr(self.scenario)}, model={repr(self.model)}, memory_plan={repr(self.memory_plan)}, current_answers={repr(self.current_answers)}, iteration{repr(self.iteration)}, additional_prompt_data={repr(self.additional_prompt_data)}, cache={repr(self.cache)}, sidecarmodel={repr(self.sidecar_model)})"
142
76
 
143
- def get_failed_task_result(self, failure_reason) -> EDSLResultObjectInput:
77
+ def get_failed_task_result(self) -> AgentResponseDict:
144
78
  """Return an AgentResponseDict used in case the question-asking fails.
145
79
 
146
- Possible reasons include:
147
- - Legimately skipped because of skip logic
148
- - Failed to get response from the model
149
-
80
+ >>> InvigilatorBase.example().get_failed_task_result()
81
+ {'answer': None, 'comment': 'Failed to get response', ...}
150
82
  """
151
- data = {
152
- "answer": None,
153
- "generated_tokens": None,
154
- "comment": failure_reason,
155
- "question_name": self.question.question_name,
156
- "prompts": self.get_prompts(),
157
- "cached_response": None,
158
- "raw_model_response": None,
159
- "cache_used": None,
160
- "cache_key": None,
161
- }
162
- return EDSLResultObjectInput(**data)
163
-
164
- # breakpoint()
165
- # if hasattr(self, "augmented_model_response"):
166
- # import json
167
-
168
- # generated_tokens = json.loads(self.augmented_model_response)["answer"][
169
- # "generated_tokens"
170
- # ]
171
- # else:
172
- # generated_tokens = "Filled in by InvigilatorBase.get_failed_task_result"
173
- # agent_response_dict = AgentResponseDict(
174
- # answer=None,
175
- # comment="Failed to get usable response",
176
- # generated_tokens=generated_tokens,
177
- # question_name=self.question.question_name,
178
- # prompts=self.get_prompts(),
179
- # )
180
- # # breakpoint()
181
- # return agent_response_dict
83
+ return AgentResponseDict(
84
+ answer=None,
85
+ comment="Failed to get response",
86
+ question_name=self.question.question_name,
87
+ prompts=self.get_prompts(),
88
+ )
182
89
 
183
90
  def get_prompts(self) -> Dict[str, Prompt]:
184
91
  """Return the prompt used."""
@@ -204,10 +111,24 @@ class InvigilatorBase(ABC):
204
111
 
205
112
  return main()
206
113
 
114
+ def create_memory_prompt(self, question_name: str) -> Prompt:
115
+ """Create a memory for the agent.
116
+
117
+ The returns a memory prompt for the agent.
118
+
119
+ >>> i = InvigilatorBase.example()
120
+ >>> i.current_answers = {"q0": "Prior answer"}
121
+ >>> i.memory_plan.add_single_memory("q1", "q0")
122
+ >>> p = i.create_memory_prompt("q1")
123
+ >>> p.text.strip().replace("\\n", " ").replace("\\t", " ")
124
+ 'Before the question you are now answering, you already answered the following question(s): Question: Do you like school? Answer: Prior answer'
125
+ """
126
+ return self.memory_plan.get_memory_prompt_fragment(
127
+ question_name, self.current_answers
128
+ )
129
+
207
130
  @classmethod
208
- def example(
209
- cls, throw_an_exception=False, question=None, scenario=None, survey=None
210
- ) -> "InvigilatorBase":
131
+ def example(cls, throw_an_exception=False, question=None, scenario=None):
211
132
  """Return an example invigilator.
212
133
 
213
134
  >>> InvigilatorBase.example()
@@ -222,53 +143,43 @@ class InvigilatorBase(ABC):
222
143
 
223
144
  from edsl.enums import InferenceServiceType
224
145
 
225
- from edsl import Model
226
-
227
- model = Model("test", canned_response="SPAM!")
228
- # class TestLanguageModelGood(LanguageModel):
229
- # """A test language model."""
146
+ class TestLanguageModelGood(LanguageModel):
147
+ """A test language model."""
230
148
 
231
- # _model_ = "test"
232
- # _parameters_ = {"temperature": 0.5}
233
- # _inference_service_ = InferenceServiceType.TEST.value
149
+ _model_ = "test"
150
+ _parameters_ = {"temperature": 0.5}
151
+ _inference_service_ = InferenceServiceType.TEST.value
234
152
 
235
- # async def async_execute_model_call(
236
- # self, user_prompt: str, system_prompt: str
237
- # ) -> dict[str, Any]:
238
- # await asyncio.sleep(0.1)
239
- # if hasattr(self, "throw_an_exception"):
240
- # raise Exception("Error!")
241
- # return {"message": """{"answer": "SPAM!"}"""}
153
+ async def async_execute_model_call(
154
+ self, user_prompt: str, system_prompt: str
155
+ ) -> dict[str, Any]:
156
+ await asyncio.sleep(0.1)
157
+ if hasattr(self, "throw_an_exception"):
158
+ raise Exception("Error!")
159
+ return {"message": """{"answer": "SPAM!"}"""}
242
160
 
243
- # def parse_response(self, raw_response: dict[str, Any]) -> str:
244
- # """Parse the response from the model."""
245
- # return raw_response["message"]
161
+ def parse_response(self, raw_response: dict[str, Any]) -> str:
162
+ """Parse the response from the model."""
163
+ return raw_response["message"]
246
164
 
165
+ model = TestLanguageModelGood()
247
166
  if throw_an_exception:
248
167
  model.throw_an_exception = True
249
168
  agent = Agent.example()
250
169
  # question = QuestionMultipleChoice.example()
251
170
  from edsl.surveys import Survey
252
171
 
253
- if not survey:
254
- survey = Survey.example()
255
- # if question:
256
- # need to have the focal question name in the list of names
257
- # survey._questions[0].question_name = question.question_name
258
- # survey.add_question(question)
259
- if question:
260
- survey.add_question(question)
261
-
172
+ survey = Survey.example()
262
173
  question = question or survey.questions[0]
263
174
  scenario = scenario or Scenario.example()
264
175
  # memory_plan = None #memory_plan = MemoryPlan()
265
176
  from edsl import Survey
266
177
 
267
- memory_plan = MemoryPlan(survey=survey)
178
+ memory_plan = MemoryPlan(survey=Survey.example())
268
179
  current_answers = None
269
- from edsl.agents.PromptConstructor import PromptConstructor
180
+ from edsl.agents.PromptConstructionMixin import PromptConstructorMixin
270
181
 
271
- class InvigilatorExample(InvigilatorBase):
182
+ class InvigilatorExample(PromptConstructorMixin, InvigilatorBase):
272
183
  """An example invigilator."""
273
184
 
274
185
  async def async_answer_question(self):
@@ -1,15 +1,15 @@
1
- from __future__ import annotations
2
- from typing import Dict, Any, Optional, Set
1
+ from typing import Dict, Any, Optional
3
2
  from collections import UserList
4
- import enum
5
-
6
- from jinja2 import Environment, meta
7
3
 
4
+ # from functools import reduce
8
5
  from edsl.prompts.Prompt import Prompt
9
- from edsl.data_transfer_models import ImageInfo
6
+
7
+ # from edsl.utilities.decorators import sync_wrapper, jupyter_nb_handler
10
8
  from edsl.prompts.registry import get_classes as prompt_lookup
11
9
  from edsl.exceptions import QuestionScenarioRenderError
12
10
 
11
+ import enum
12
+
13
13
 
14
14
  class PromptComponent(enum.Enum):
15
15
  AGENT_INSTRUCTIONS = "agent_instructions"
@@ -18,21 +18,6 @@ class PromptComponent(enum.Enum):
18
18
  PRIOR_QUESTION_MEMORY = "prior_question_memory"
19
19
 
20
20
 
21
- def get_jinja2_variables(template_str: str) -> Set[str]:
22
- """
23
- Extracts all variable names from a Jinja2 template using Jinja2's built-in parsing.
24
-
25
- Args:
26
- template_str (str): The Jinja2 template string
27
-
28
- Returns:
29
- Set[str]: A set of variable names found in the template
30
- """
31
- env = Environment()
32
- ast = env.parse(template_str)
33
- return meta.find_undeclared_variables(ast)
34
-
35
-
36
21
  class PromptList(UserList):
37
22
  separator = Prompt(" ")
38
23
 
@@ -151,7 +136,7 @@ class PromptPlan:
151
136
  }
152
137
 
153
138
 
154
- class PromptConstructor:
139
+ class PromptConstructorMixin:
155
140
  """Mixin for constructing prompts for the LLM call.
156
141
 
157
142
  The pieces of a prompt are:
@@ -163,40 +148,16 @@ class PromptConstructor:
163
148
  This is mixed into the Invigilator class.
164
149
  """
165
150
 
166
- def __init__(self, invigilator):
167
- self.invigilator = invigilator
168
- self.agent = invigilator.agent
169
- self.question = invigilator.question
170
- self.scenario = invigilator.scenario
171
- self.survey = invigilator.survey
172
- self.model = invigilator.model
173
- self.current_answers = invigilator.current_answers
174
- self.memory_plan = invigilator.memory_plan
175
- self.prompt_plan = PromptPlan() # Assuming PromptPlan is defined elsewhere
176
-
177
- # prompt_plan = PromptPlan()
178
-
179
- @property
180
- def scenario_image_keys(self):
181
- image_entries = []
182
-
183
- for key, value in self.scenario.items():
184
- if isinstance(value, ImageInfo):
185
- image_entries.append(key)
186
- return image_entries
151
+ prompt_plan = PromptPlan()
187
152
 
188
153
  @property
189
154
  def agent_instructions_prompt(self) -> Prompt:
190
155
  """
191
156
  >>> from edsl.agents.InvigilatorBase import InvigilatorBase
192
157
  >>> i = InvigilatorBase.example()
193
- >>> i.prompt_constructor.agent_instructions_prompt
158
+ >>> i.agent_instructions_prompt
194
159
  Prompt(text=\"""You are answering questions as if you were a human. Do not break character.\""")
195
160
  """
196
- from edsl import Agent
197
-
198
- if self.agent == Agent(): # if agent is empty, then return an empty prompt
199
- return Prompt(text="")
200
161
  if not hasattr(self, "_agent_instructions_prompt"):
201
162
  applicable_prompts = prompt_lookup(
202
163
  component_type="agent_instructions",
@@ -214,17 +175,12 @@ class PromptConstructor:
214
175
  """
215
176
  >>> from edsl.agents.InvigilatorBase import InvigilatorBase
216
177
  >>> i = InvigilatorBase.example()
217
- >>> i.prompt_constructor.agent_persona_prompt
178
+ >>> i.agent_persona_prompt
218
179
  Prompt(text=\"""You are an agent with the following persona:
219
180
  {'age': 22, 'hair': 'brown', 'height': 5.5}\""")
220
181
 
221
182
  """
222
183
  if not hasattr(self, "_agent_persona_prompt"):
223
- from edsl import Agent
224
-
225
- if self.agent == Agent(): # if agent is empty, then return an empty prompt
226
- return Prompt(text="")
227
-
228
184
  if not hasattr(self.agent, "agent_persona"):
229
185
  applicable_prompts = prompt_lookup(
230
186
  component_type="agent_persona",
@@ -269,69 +225,92 @@ class PromptConstructor:
269
225
  d[new_question].comment = answer
270
226
  return d
271
227
 
272
- @property
273
- def question_image_keys(self):
274
- raw_question_text = self.question.question_text
275
- variables = get_jinja2_variables(raw_question_text)
276
- question_image_keys = []
277
- for var in variables:
278
- if var in self.scenario_image_keys:
279
- question_image_keys.append(var)
280
- return question_image_keys
281
-
282
228
  @property
283
229
  def question_instructions_prompt(self) -> Prompt:
284
230
  """
285
231
  >>> from edsl.agents.InvigilatorBase import InvigilatorBase
286
232
  >>> i = InvigilatorBase.example()
287
- >>> i.prompt_constructor.question_instructions_prompt
288
- Prompt(text=\"""...
233
+ >>> i.question_instructions_prompt
234
+ Prompt(text=\"""You are being asked the following question: Do you like school?
235
+ The options are
236
+ <BLANKLINE>
237
+ 0: yes
238
+ <BLANKLINE>
239
+ 1: no
240
+ <BLANKLINE>
241
+ Return a valid JSON formatted like this, selecting only the number of the option:
242
+ {"answer": <put answer code here>, "comment": "<put explanation here>"}
243
+ Only 1 option may be selected.\""")
244
+
245
+ >>> from edsl import QuestionFreeText
246
+ >>> q = QuestionFreeText(question_text = "Consider {{ X }}. What is your favorite color?", question_name = "q_color")
247
+ >>> from edsl.agents.InvigilatorBase import InvigilatorBase
248
+ >>> i = InvigilatorBase.example(question = q)
249
+ >>> i.question_instructions_prompt
250
+ Traceback (most recent call last):
289
251
  ...
252
+ edsl.exceptions.questions.QuestionScenarioRenderError: Question instructions still has variables: ['X'].
253
+
254
+
255
+ >>> from edsl import QuestionFreeText
256
+ >>> q = QuestionFreeText(question_text = "You were asked the question '{{ q0.question_text }}'. What is your favorite color?", question_name = "q_color")
257
+ >>> from edsl.agents.InvigilatorBase import InvigilatorBase
258
+ >>> i = InvigilatorBase.example(question = q)
259
+ >>> i.question_instructions_prompt
260
+ Prompt(text=\"""You are being asked the following question: You were asked the question 'Do you like school?'. What is your favorite color?
261
+ Return a valid JSON formatted like this:
262
+ {"answer": "<put free text answer here>"}\""")
263
+
264
+ >>> from edsl import QuestionFreeText
265
+ >>> q = QuestionFreeText(question_text = "You stated '{{ q0.answer }}'. What is your favorite color?", question_name = "q_color")
266
+ >>> from edsl.agents.InvigilatorBase import InvigilatorBase
267
+ >>> i = InvigilatorBase.example(question = q)
268
+ >>> i.current_answers = {"q0": "I like school"}
269
+ >>> i.question_instructions_prompt
270
+ Prompt(text=\"""You are being asked the following question: You stated 'I like school'. What is your favorite color?
271
+ Return a valid JSON formatted like this:
272
+ {"answer": "<put free text answer here>"}\""")
273
+
274
+
290
275
  """
291
276
  if not hasattr(self, "_question_instructions_prompt"):
292
277
  question_prompt = self.question.get_instructions(model=self.model.model)
293
278
 
294
- # Are any of the scenario values ImageInfo
279
+ # TODO: Try to populate the answers in the question object if they are available
280
+ # d = self.survey.question_names_to_questions()
281
+ # for question, answer in self.current_answers.items():
282
+ # if question in d:
283
+ # d[question].answer = answer
284
+ # else:
285
+ # # adds a comment to the question
286
+ # if (new_question := question.split("_comment")[0]) in d:
287
+ # d[new_question].comment = answer
295
288
 
296
289
  question_data = self.question.data.copy()
297
290
 
298
291
  # check to see if the question_options is actually a string
299
292
  # This is used when the user is using the question_options as a variable from a sceario
300
- # if "question_options" in question_data:
301
- if isinstance(self.question.data.get("question_options", None), str):
302
- env = Environment()
303
- parsed_content = env.parse(self.question.data["question_options"])
304
- question_option_key = list(
305
- meta.find_undeclared_variables(parsed_content)
306
- )[0]
307
-
308
- if isinstance(
309
- question_options := self.scenario.get(question_option_key), list
310
- ):
311
- question_data["question_options"] = question_options
312
- self.question.question_options = question_options
313
-
314
- replacement_dict = (
315
- {key: "<see image>" for key in self.scenario_image_keys}
316
- | question_data
317
- | {
318
- k: v
319
- for k, v in self.scenario.items()
320
- if k not in self.scenario_image_keys
321
- } # don't include images in the replacement dict
293
+ if "question_options" in question_data:
294
+ if isinstance(self.question.data["question_options"], str):
295
+ from jinja2 import Environment, meta
296
+
297
+ env = Environment()
298
+ parsed_content = env.parse(self.question.data["question_options"])
299
+ question_option_key = list(
300
+ meta.find_undeclared_variables(parsed_content)
301
+ )[0]
302
+ question_data["question_options"] = self.scenario.get(
303
+ question_option_key
304
+ )
305
+
306
+ # breakpoint()
307
+ rendered_instructions = question_prompt.render(
308
+ question_data
309
+ | self.scenario
322
310
  | self.prior_answers_dict()
323
311
  | {"agent": self.agent}
324
- | {
325
- "use_code": getattr(self.question, "_use_code", True),
326
- "include_comment": getattr(
327
- self.question, "_include_comment", False
328
- ),
329
- }
330
312
  )
331
313
 
332
- rendered_instructions = question_prompt.render(replacement_dict)
333
-
334
- # is there anything left to render?
335
314
  undefined_template_variables = (
336
315
  rendered_instructions.undefined_template_variables({})
337
316
  )
@@ -345,25 +324,11 @@ class PromptConstructor:
345
324
  )
346
325
 
347
326
  if undefined_template_variables:
327
+ print(undefined_template_variables)
348
328
  raise QuestionScenarioRenderError(
349
329
  f"Question instructions still has variables: {undefined_template_variables}."
350
330
  )
351
331
 
352
- ####################################
353
- # Check if question has instructions - these are instructions in a Survey that can apply to multiple follow-on questions
354
- ####################################
355
- relevant_instructions = self.survey.relevant_instructions(
356
- self.question.question_name
357
- )
358
-
359
- if relevant_instructions != []:
360
- preamble_text = Prompt(
361
- text="Before answer this question, you were given the following instructions: "
362
- )
363
- for instruction in relevant_instructions:
364
- preamble_text += instruction.text
365
- rendered_instructions = preamble_text + rendered_instructions
366
-
367
332
  self._question_instructions_prompt = rendered_instructions
368
333
  return self._question_instructions_prompt
369
334
 
@@ -380,23 +345,6 @@ class PromptConstructor:
380
345
  self._prior_question_memory_prompt = memory_prompt
381
346
  return self._prior_question_memory_prompt
382
347
 
383
- def create_memory_prompt(self, question_name: str) -> Prompt:
384
- """Create a memory for the agent.
385
-
386
- The returns a memory prompt for the agent.
387
-
388
- >>> from edsl.agents.InvigilatorBase import InvigilatorBase
389
- >>> i = InvigilatorBase.example()
390
- >>> i.current_answers = {"q0": "Prior answer"}
391
- >>> i.memory_plan.add_single_memory("q1", "q0")
392
- >>> p = i.prompt_constructor.create_memory_prompt("q1")
393
- >>> p.text.strip().replace("\\n", " ").replace("\\t", " ")
394
- 'Before the question you are now answering, you already answered the following question(s): Question: Do you like school? Answer: Prior answer'
395
- """
396
- return self.memory_plan.get_memory_prompt_fragment(
397
- question_name, self.current_answers
398
- )
399
-
400
348
  def construct_system_prompt(self) -> Prompt:
401
349
  """Construct the system prompt for the LLM call."""
402
350
  import warnings
@@ -420,10 +368,17 @@ class PromptConstructor:
420
368
 
421
369
  >>> from edsl import QuestionFreeText
422
370
  >>> from edsl.agents.InvigilatorBase import InvigilatorBase
423
- >>> q = QuestionFreeText(question_text="How are you today?", question_name="q_new")
371
+ >>> q = QuestionFreeText(question_text="How are you today?", question_name="q0")
424
372
  >>> i = InvigilatorBase.example(question = q)
425
373
  >>> i.get_prompts()
426
374
  {'user_prompt': ..., 'system_prompt': ...}
375
+ >>> scenario = i._get_scenario_with_image()
376
+ >>> scenario.has_image
377
+ True
378
+ >>> q = QuestionFreeText(question_text="How are you today?", question_name="q0")
379
+ >>> i = InvigilatorBase.example(question = q, scenario = scenario)
380
+ >>> i.get_prompts()
381
+ {'user_prompt': ..., 'system_prompt': ..., 'encoded_image': ...'}
427
382
  """
428
383
  prompts = self.prompt_plan.get_prompts(
429
384
  agent_instructions=self.agent_instructions_prompt,
@@ -431,16 +386,12 @@ class PromptConstructor:
431
386
  question_instructions=self.question_instructions_prompt,
432
387
  prior_question_memory=self.prior_question_memory_prompt,
433
388
  )
434
- if len(self.question_image_keys) > 1:
435
- raise ValueError("We can only handle one image per question.")
436
- elif len(self.question_image_keys) == 1:
437
- prompts["encoded_image"] = self.scenario[
438
- self.question_image_keys[0]
439
- ].encoded_image
440
389
 
390
+ if hasattr(self.scenario, "has_image") and self.scenario.has_image:
391
+ prompts["encoded_image"] = self.scenario["encoded_image"]
441
392
  return prompts
442
393
 
443
- def _get_scenario_with_image(self) -> Scenario:
394
+ def _get_scenario_with_image(self) -> Dict[str, Any]:
444
395
  """This is a helper function to get a scenario with an image, for testing purposes."""
445
396
  from edsl import Scenario
446
397
 
edsl/agents/__init__.py CHANGED
@@ -1,3 +1,2 @@
1
1
  from edsl.agents.Agent import Agent
2
2
  from edsl.agents.AgentList import AgentList
3
- from edsl.agents.InvigilatorBase import InvigilatorBase