edsl 0.1.32__py3-none-any.whl → 0.1.33__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 (181) hide show
  1. edsl/Base.py +9 -3
  2. edsl/TemplateLoader.py +24 -0
  3. edsl/__init__.py +8 -3
  4. edsl/__version__.py +1 -1
  5. edsl/agents/Agent.py +40 -8
  6. edsl/agents/AgentList.py +43 -0
  7. edsl/agents/Invigilator.py +135 -219
  8. edsl/agents/InvigilatorBase.py +148 -59
  9. edsl/agents/{PromptConstructionMixin.py → PromptConstructor.py} +138 -89
  10. edsl/agents/__init__.py +1 -0
  11. edsl/auto/AutoStudy.py +117 -0
  12. edsl/auto/StageBase.py +230 -0
  13. edsl/auto/StageGenerateSurvey.py +178 -0
  14. edsl/auto/StageLabelQuestions.py +125 -0
  15. edsl/auto/StagePersona.py +61 -0
  16. edsl/auto/StagePersonaDimensionValueRanges.py +88 -0
  17. edsl/auto/StagePersonaDimensionValues.py +74 -0
  18. edsl/auto/StagePersonaDimensions.py +69 -0
  19. edsl/auto/StageQuestions.py +73 -0
  20. edsl/auto/SurveyCreatorPipeline.py +21 -0
  21. edsl/auto/utilities.py +224 -0
  22. edsl/config.py +47 -56
  23. edsl/coop/PriceFetcher.py +58 -0
  24. edsl/coop/coop.py +50 -7
  25. edsl/data/Cache.py +35 -1
  26. edsl/data_transfer_models.py +73 -38
  27. edsl/enums.py +4 -0
  28. edsl/exceptions/language_models.py +25 -1
  29. edsl/exceptions/questions.py +62 -5
  30. edsl/exceptions/results.py +4 -0
  31. edsl/inference_services/AnthropicService.py +13 -11
  32. edsl/inference_services/AwsBedrock.py +19 -17
  33. edsl/inference_services/AzureAI.py +37 -20
  34. edsl/inference_services/GoogleService.py +16 -12
  35. edsl/inference_services/GroqService.py +2 -0
  36. edsl/inference_services/InferenceServiceABC.py +58 -3
  37. edsl/inference_services/MistralAIService.py +120 -0
  38. edsl/inference_services/OpenAIService.py +48 -54
  39. edsl/inference_services/TestService.py +80 -0
  40. edsl/inference_services/TogetherAIService.py +170 -0
  41. edsl/inference_services/models_available_cache.py +0 -6
  42. edsl/inference_services/registry.py +6 -0
  43. edsl/jobs/Answers.py +10 -12
  44. edsl/jobs/FailedQuestion.py +78 -0
  45. edsl/jobs/Jobs.py +37 -22
  46. edsl/jobs/buckets/BucketCollection.py +24 -15
  47. edsl/jobs/buckets/TokenBucket.py +93 -14
  48. edsl/jobs/interviews/Interview.py +366 -78
  49. edsl/jobs/interviews/{interview_exception_tracking.py → InterviewExceptionCollection.py} +14 -68
  50. edsl/jobs/interviews/InterviewExceptionEntry.py +85 -19
  51. edsl/jobs/runners/JobsRunnerAsyncio.py +146 -175
  52. edsl/jobs/runners/JobsRunnerStatus.py +331 -0
  53. edsl/jobs/tasks/QuestionTaskCreator.py +30 -23
  54. edsl/jobs/tasks/TaskHistory.py +148 -213
  55. edsl/language_models/LanguageModel.py +261 -156
  56. edsl/language_models/ModelList.py +2 -2
  57. edsl/language_models/RegisterLanguageModelsMeta.py +14 -29
  58. edsl/language_models/fake_openai_call.py +15 -0
  59. edsl/language_models/fake_openai_service.py +61 -0
  60. edsl/language_models/registry.py +23 -6
  61. edsl/language_models/repair.py +0 -19
  62. edsl/language_models/utilities.py +61 -0
  63. edsl/notebooks/Notebook.py +20 -2
  64. edsl/prompts/Prompt.py +52 -2
  65. edsl/questions/AnswerValidatorMixin.py +23 -26
  66. edsl/questions/QuestionBase.py +330 -249
  67. edsl/questions/QuestionBaseGenMixin.py +133 -0
  68. edsl/questions/QuestionBasePromptsMixin.py +266 -0
  69. edsl/questions/QuestionBudget.py +99 -41
  70. edsl/questions/QuestionCheckBox.py +227 -35
  71. edsl/questions/QuestionExtract.py +98 -27
  72. edsl/questions/QuestionFreeText.py +52 -29
  73. edsl/questions/QuestionFunctional.py +7 -0
  74. edsl/questions/QuestionList.py +141 -22
  75. edsl/questions/QuestionMultipleChoice.py +159 -65
  76. edsl/questions/QuestionNumerical.py +88 -46
  77. edsl/questions/QuestionRank.py +182 -24
  78. edsl/questions/Quick.py +41 -0
  79. edsl/questions/RegisterQuestionsMeta.py +31 -12
  80. edsl/questions/ResponseValidatorABC.py +170 -0
  81. edsl/questions/__init__.py +3 -4
  82. edsl/questions/decorators.py +21 -0
  83. edsl/questions/derived/QuestionLikertFive.py +10 -5
  84. edsl/questions/derived/QuestionLinearScale.py +15 -2
  85. edsl/questions/derived/QuestionTopK.py +10 -1
  86. edsl/questions/derived/QuestionYesNo.py +24 -3
  87. edsl/questions/descriptors.py +43 -7
  88. edsl/questions/prompt_templates/question_budget.jinja +13 -0
  89. edsl/questions/prompt_templates/question_checkbox.jinja +32 -0
  90. edsl/questions/prompt_templates/question_extract.jinja +11 -0
  91. edsl/questions/prompt_templates/question_free_text.jinja +3 -0
  92. edsl/questions/prompt_templates/question_linear_scale.jinja +11 -0
  93. edsl/questions/prompt_templates/question_list.jinja +17 -0
  94. edsl/questions/prompt_templates/question_multiple_choice.jinja +33 -0
  95. edsl/questions/prompt_templates/question_numerical.jinja +37 -0
  96. edsl/questions/question_registry.py +6 -2
  97. edsl/questions/templates/__init__.py +0 -0
  98. edsl/questions/templates/budget/__init__.py +0 -0
  99. edsl/questions/templates/budget/answering_instructions.jinja +7 -0
  100. edsl/questions/templates/budget/question_presentation.jinja +7 -0
  101. edsl/questions/templates/checkbox/__init__.py +0 -0
  102. edsl/questions/templates/checkbox/answering_instructions.jinja +10 -0
  103. edsl/questions/templates/checkbox/question_presentation.jinja +22 -0
  104. edsl/questions/templates/extract/__init__.py +0 -0
  105. edsl/questions/templates/extract/answering_instructions.jinja +7 -0
  106. edsl/questions/templates/extract/question_presentation.jinja +1 -0
  107. edsl/questions/templates/free_text/__init__.py +0 -0
  108. edsl/questions/templates/free_text/answering_instructions.jinja +0 -0
  109. edsl/questions/templates/free_text/question_presentation.jinja +1 -0
  110. edsl/questions/templates/likert_five/__init__.py +0 -0
  111. edsl/questions/templates/likert_five/answering_instructions.jinja +10 -0
  112. edsl/questions/templates/likert_five/question_presentation.jinja +12 -0
  113. edsl/questions/templates/linear_scale/__init__.py +0 -0
  114. edsl/questions/templates/linear_scale/answering_instructions.jinja +5 -0
  115. edsl/questions/templates/linear_scale/question_presentation.jinja +5 -0
  116. edsl/questions/templates/list/__init__.py +0 -0
  117. edsl/questions/templates/list/answering_instructions.jinja +4 -0
  118. edsl/questions/templates/list/question_presentation.jinja +5 -0
  119. edsl/questions/templates/multiple_choice/__init__.py +0 -0
  120. edsl/questions/templates/multiple_choice/answering_instructions.jinja +9 -0
  121. edsl/questions/templates/multiple_choice/html.jinja +0 -0
  122. edsl/questions/templates/multiple_choice/question_presentation.jinja +12 -0
  123. edsl/questions/templates/numerical/__init__.py +0 -0
  124. edsl/questions/templates/numerical/answering_instructions.jinja +8 -0
  125. edsl/questions/templates/numerical/question_presentation.jinja +7 -0
  126. edsl/questions/templates/rank/__init__.py +0 -0
  127. edsl/questions/templates/rank/answering_instructions.jinja +11 -0
  128. edsl/questions/templates/rank/question_presentation.jinja +15 -0
  129. edsl/questions/templates/top_k/__init__.py +0 -0
  130. edsl/questions/templates/top_k/answering_instructions.jinja +8 -0
  131. edsl/questions/templates/top_k/question_presentation.jinja +22 -0
  132. edsl/questions/templates/yes_no/__init__.py +0 -0
  133. edsl/questions/templates/yes_no/answering_instructions.jinja +6 -0
  134. edsl/questions/templates/yes_no/question_presentation.jinja +12 -0
  135. edsl/results/Dataset.py +20 -0
  136. edsl/results/DatasetExportMixin.py +46 -48
  137. edsl/results/DatasetTree.py +145 -0
  138. edsl/results/Result.py +32 -5
  139. edsl/results/Results.py +135 -46
  140. edsl/results/ResultsDBMixin.py +3 -3
  141. edsl/results/Selector.py +118 -0
  142. edsl/results/tree_explore.py +115 -0
  143. edsl/scenarios/FileStore.py +71 -10
  144. edsl/scenarios/Scenario.py +96 -25
  145. edsl/scenarios/ScenarioImageMixin.py +2 -2
  146. edsl/scenarios/ScenarioList.py +361 -39
  147. edsl/scenarios/ScenarioListExportMixin.py +9 -0
  148. edsl/scenarios/ScenarioListPdfMixin.py +150 -4
  149. edsl/study/SnapShot.py +8 -1
  150. edsl/study/Study.py +32 -0
  151. edsl/surveys/Rule.py +10 -1
  152. edsl/surveys/RuleCollection.py +21 -5
  153. edsl/surveys/Survey.py +637 -311
  154. edsl/surveys/SurveyExportMixin.py +71 -9
  155. edsl/surveys/SurveyFlowVisualizationMixin.py +2 -1
  156. edsl/surveys/SurveyQualtricsImport.py +75 -4
  157. edsl/surveys/instructions/ChangeInstruction.py +47 -0
  158. edsl/surveys/instructions/Instruction.py +34 -0
  159. edsl/surveys/instructions/InstructionCollection.py +77 -0
  160. edsl/surveys/instructions/__init__.py +0 -0
  161. edsl/templates/error_reporting/base.html +24 -0
  162. edsl/templates/error_reporting/exceptions_by_model.html +35 -0
  163. edsl/templates/error_reporting/exceptions_by_question_name.html +17 -0
  164. edsl/templates/error_reporting/exceptions_by_type.html +17 -0
  165. edsl/templates/error_reporting/interview_details.html +116 -0
  166. edsl/templates/error_reporting/interviews.html +10 -0
  167. edsl/templates/error_reporting/overview.html +5 -0
  168. edsl/templates/error_reporting/performance_plot.html +2 -0
  169. edsl/templates/error_reporting/report.css +74 -0
  170. edsl/templates/error_reporting/report.html +118 -0
  171. edsl/templates/error_reporting/report.js +25 -0
  172. edsl/utilities/utilities.py +9 -1
  173. {edsl-0.1.32.dist-info → edsl-0.1.33.dist-info}/METADATA +5 -2
  174. edsl-0.1.33.dist-info/RECORD +295 -0
  175. edsl/jobs/interviews/InterviewTaskBuildingMixin.py +0 -286
  176. edsl/jobs/interviews/retry_management.py +0 -37
  177. edsl/jobs/runners/JobsRunnerStatusMixin.py +0 -333
  178. edsl/utilities/gcp_bucket/simple_example.py +0 -9
  179. edsl-0.1.32.dist-info/RECORD +0 -209
  180. {edsl-0.1.32.dist-info → edsl-0.1.33.dist-info}/LICENSE +0 -0
  181. {edsl-0.1.32.dist-info → edsl-0.1.33.dist-info}/WHEEL +0 -0
@@ -0,0 +1,133 @@
1
+ from __future__ import annotations
2
+ import copy
3
+ import itertools
4
+ from typing import Optional, List, Callable, Type
5
+ from typing import TypeVar
6
+
7
+
8
+ class QuestionBaseGenMixin:
9
+ def copy(self) -> QuestionBase:
10
+ """Return a deep copy of the question.
11
+
12
+ >>> from edsl.questions import QuestionFreeText
13
+ >>> q = QuestionFreeText(question_name = "color", question_text = "What is your favorite color?")
14
+ >>> q2 = q.copy()
15
+ >>> q2.question_name
16
+ 'color'
17
+
18
+ """
19
+ return copy.deepcopy(self)
20
+
21
+ def option_permutations(self) -> list[QuestionBase]:
22
+ """Return a list of questions with all possible permutations of the options.
23
+
24
+ >>> from edsl import QuestionMultipleChoice as Q
25
+ >>> len(Q.example().option_permutations())
26
+ 24
27
+ """
28
+
29
+ if not hasattr(self, "question_options"):
30
+ return [self]
31
+
32
+ questions = []
33
+ for index, permutation in enumerate(
34
+ itertools.permutations(self.question_options)
35
+ ):
36
+ question = copy.deepcopy(self)
37
+ question.question_options = list(permutation)
38
+ question.question_name = f"{self.question_name}_{index}"
39
+ questions.append(question)
40
+ return questions
41
+
42
+ def loop(self, scenario_list: ScenarioList) -> List[QuestionBase]:
43
+ """Return a list of questions with the question name modified for each scenario.
44
+
45
+ :param scenario_list: The list of scenarios to loop through.
46
+
47
+ >>> from edsl import QuestionFreeText
48
+ >>> from edsl import ScenarioList
49
+ >>> q = QuestionFreeText(question_text = "What are your thoughts on: {{ subject}}?", question_name = "base_{{subject}}")
50
+ >>> len(q.loop(ScenarioList.from_list("subject", ["Math", "Economics", "Chemistry"])))
51
+ 3
52
+
53
+ """
54
+ from jinja2 import Environment
55
+ from edsl.questions.QuestionBase import QuestionBase
56
+
57
+ starting_name = self.question_name
58
+ questions = []
59
+ for index, scenario in enumerate(scenario_list):
60
+ env = Environment()
61
+ new_data = self.to_dict().copy()
62
+ for key, value in [(k, v) for k, v in new_data.items() if v is not None]:
63
+ if (
64
+ isinstance(value, str) or isinstance(value, int)
65
+ ) and key != "question_options":
66
+ new_data[key] = env.from_string(value).render(scenario)
67
+ elif isinstance(value, list):
68
+ new_data[key] = [
69
+ env.from_string(v).render(scenario) if isinstance(v, str) else v
70
+ for v in value
71
+ ]
72
+ elif isinstance(value, dict):
73
+ new_data[key] = {
74
+ (
75
+ env.from_string(k).render(scenario)
76
+ if isinstance(k, str)
77
+ else k
78
+ ): (
79
+ env.from_string(v).render(scenario)
80
+ if isinstance(v, str)
81
+ else v
82
+ )
83
+ for k, v in value.items()
84
+ }
85
+ elif key == "question_options" and isinstance(value, str):
86
+ new_data[key] = value
87
+ else:
88
+ raise ValueError(
89
+ f"Unexpected value type: {type(value)} for key '{key}'"
90
+ )
91
+
92
+ if new_data["question_name"] == starting_name:
93
+ new_data["question_name"] = new_data["question_name"] + f"_{index}"
94
+
95
+ questions.append(QuestionBase.from_dict(new_data))
96
+ return questions
97
+
98
+ def apply_function(self, func: Callable, exclude_components=None) -> QuestionBase:
99
+ """Apply a function to the question parts
100
+
101
+ >>> from edsl.questions import QuestionFreeText
102
+ >>> q = QuestionFreeText(question_name = "color", question_text = "What is your favorite color?")
103
+ >>> shouting = lambda x: x.upper()
104
+ >>> q.apply_function(shouting)
105
+ Question('free_text', question_name = \"""color\""", question_text = \"""WHAT IS YOUR FAVORITE COLOR?\""")
106
+
107
+ """
108
+ from edsl.questions.QuestionBase import QuestionBase
109
+
110
+ if exclude_components is None:
111
+ exclude_components = ["question_name", "question_type"]
112
+
113
+ d = copy.deepcopy(self._to_dict())
114
+ for key, value in d.items():
115
+ if key in exclude_components:
116
+ continue
117
+ if isinstance(value, dict):
118
+ for k, v in value.items():
119
+ value[k] = func(v)
120
+ d[key] = value
121
+ continue
122
+ if isinstance(value, list):
123
+ value = [func(v) for v in value]
124
+ d[key] = value
125
+ continue
126
+ d[key] = func(value)
127
+ return QuestionBase.from_dict(d)
128
+
129
+
130
+ if __name__ == "__main__":
131
+ import doctest
132
+
133
+ doctest.testmod()
@@ -0,0 +1,266 @@
1
+ from importlib import resources
2
+ from typing import Optional
3
+ from edsl.prompts import Prompt
4
+ from edsl.exceptions.questions import QuestionAnswerValidationError
5
+
6
+ from functools import lru_cache
7
+
8
+
9
+ class TemplateManager:
10
+ _instance = None
11
+
12
+ def __new__(cls):
13
+ if cls._instance is None:
14
+ cls._instance = super().__new__(cls)
15
+ cls._instance._template_cache = {}
16
+ return cls._instance
17
+
18
+ @lru_cache(maxsize=None)
19
+ def get_template(self, question_type, template_name):
20
+ if (question_type, template_name) not in self._template_cache:
21
+ with resources.open_text(
22
+ f"edsl.questions.templates.{question_type}", template_name
23
+ ) as file:
24
+ self._template_cache[(question_type, template_name)] = file.read()
25
+ return self._template_cache[(question_type, template_name)]
26
+
27
+
28
+ # Global instance
29
+ template_manager = TemplateManager()
30
+
31
+
32
+ class QuestionBasePromptsMixin:
33
+ # @classmethod
34
+ # @lru_cache(maxsize=1)
35
+ # def _read_template(cls, template_name):
36
+ # with resources.open_text(
37
+ # f"edsl.questions.templates.{cls.question_type}", template_name
38
+ # ) as file:
39
+ # return file.read()
40
+
41
+ @classmethod
42
+ def applicable_prompts(
43
+ cls, model: Optional[str] = None
44
+ ) -> list[type["PromptBase"]]:
45
+ """Get the prompts that are applicable to the question type.
46
+
47
+ :param model: The language model to use.
48
+
49
+ >>> from edsl.questions import QuestionFreeText
50
+ >>> QuestionFreeText.applicable_prompts()
51
+ [<class 'edsl.prompts.library.question_freetext.FreeText'>]
52
+
53
+ :param model: The language model to use. If None, assumes does not matter.
54
+
55
+ """
56
+ from edsl.prompts.registry import get_classes as prompt_lookup
57
+
58
+ applicable_prompts = prompt_lookup(
59
+ component_type="question_instructions",
60
+ question_type=cls.question_type,
61
+ model=model,
62
+ )
63
+ return applicable_prompts
64
+
65
+ @property
66
+ def model_instructions(self) -> dict:
67
+ """Get the model-specific instructions for the question."""
68
+ if not hasattr(self, "_model_instructions"):
69
+ self._model_instructions = {}
70
+ return self._model_instructions
71
+
72
+ def _all_text(self) -> str:
73
+ """Return the question text.
74
+
75
+ >>> from edsl import QuestionMultipleChoice as Q
76
+ >>> Q.example()._all_text()
77
+ "how_feelingHow are you?['Good', 'Great', 'OK', 'Bad']"
78
+ """
79
+ txt = ""
80
+ for key, value in self.data.items():
81
+ if isinstance(value, str):
82
+ txt += value
83
+ elif isinstance(value, list):
84
+ txt += "".join(str(value))
85
+ return txt
86
+
87
+ @model_instructions.setter
88
+ def model_instructions(self, data: dict):
89
+ """Set the model-specific instructions for the question."""
90
+ self._model_instructions = data
91
+
92
+ def add_model_instructions(
93
+ self, *, instructions: str, model: Optional[str] = None
94
+ ) -> None:
95
+ """Add model-specific instructions for the question that override the default instructions.
96
+
97
+ :param instructions: The instructions to add. This is typically a jinja2 template.
98
+ :param model: The language model for this instruction.
99
+
100
+ >>> from edsl.questions import QuestionFreeText
101
+ >>> q = QuestionFreeText(question_name = "color", question_text = "What is your favorite color?")
102
+ >>> q.add_model_instructions(instructions = "{{question_text}}. Answer in valid JSON like so {'answer': 'comment: <>}", model = "gpt3")
103
+ >>> q.get_instructions(model = "gpt3")
104
+ Prompt(text=\"""{{question_text}}. Answer in valid JSON like so {'answer': 'comment: <>}\""")
105
+ """
106
+ from edsl import Model
107
+
108
+ if not hasattr(self, "_model_instructions"):
109
+ self._model_instructions = {}
110
+ if model is None:
111
+ # if not model is passed, all the models are mapped to this instruction, including 'None'
112
+ self._model_instructions = {
113
+ model_name: instructions
114
+ for model_name in Model.available(name_only=True)
115
+ }
116
+ self._model_instructions.update({model: instructions})
117
+ else:
118
+ self._model_instructions.update({model: instructions})
119
+
120
+ @classmethod
121
+ def path_to_folder(cls) -> str:
122
+ return resources.files(f"edsl.questions.templates", cls.question_type)
123
+
124
+ @property
125
+ def response_model(self) -> type["BaseModel"]:
126
+ if self._response_model is not None:
127
+ return self._response_model
128
+ else:
129
+ return self.create_response_model()
130
+
131
+ @property
132
+ def use_code(self) -> bool:
133
+ if hasattr(self, "_use_code"):
134
+ return self._use_code
135
+ return True
136
+
137
+ @use_code.setter
138
+ def use_code(self, value: bool) -> None:
139
+ self._use_code = value
140
+
141
+ @property
142
+ def include_comment(self) -> bool:
143
+ if hasattr(self, "_include_comment"):
144
+ return self._include_comment
145
+ return True
146
+
147
+ @include_comment.setter
148
+ def include_comment(self, value: bool) -> None:
149
+ self._include_comment = value
150
+
151
+ @classmethod
152
+ def default_answering_instructions(cls) -> str:
153
+ # template_text = cls._read_template("answering_instructions.jinja")
154
+ template_text = template_manager.get_template(
155
+ cls.question_type, "answering_instructions.jinja"
156
+ )
157
+ return Prompt(text=template_text)
158
+
159
+ @classmethod
160
+ def default_question_presentation(cls):
161
+ # template_text = cls._read_template("question_presentation.jinja")
162
+ template_text = template_manager.get_template(
163
+ cls.question_type, "question_presentation.jinja"
164
+ )
165
+ return Prompt(text=template_text)
166
+
167
+ @property
168
+ def answering_instructions(self) -> str:
169
+ if self._answering_instructions is None:
170
+ return self.default_answering_instructions()
171
+ return self._answering_instructions
172
+
173
+ @answering_instructions.setter
174
+ def answering_instructions(self, value) -> None:
175
+ self._answering_instructions = value
176
+
177
+ # @classmethod
178
+ # def default_answering_instructions(cls) -> str:
179
+ # with resources.open_text(
180
+ # f"edsl.questions.templates.{cls.question_type}",
181
+ # "answering_instructions.jinja",
182
+ # ) as file:
183
+ # return Prompt(text=file.read())
184
+
185
+ # @classmethod
186
+ # def default_question_presentation(cls):
187
+ # with resources.open_text(
188
+ # f"edsl.questions.templates.{cls.question_type}",
189
+ # "question_presentation.jinja",
190
+ # ) as file:
191
+ # return Prompt(text=file.read())
192
+
193
+ @property
194
+ def question_presentation(self):
195
+ if self._question_presentation is None:
196
+ return self.default_question_presentation()
197
+ return self._question_presentation
198
+
199
+ @question_presentation.setter
200
+ def question_presentation(self, value):
201
+ self._question_presentation = value
202
+
203
+ def prompt_preview(self, scenario=None, agent=None):
204
+ return self.new_default_instructions.render(
205
+ self.data
206
+ | {
207
+ "include_comment": getattr(self, "_include_comment", True),
208
+ "use_code": getattr(self, "_use_code", True),
209
+ }
210
+ | ({"scenario": scenario} or {})
211
+ | ({"agent": agent} or {})
212
+ )
213
+
214
+ @classmethod
215
+ def self_check(cls):
216
+ q = cls.example()
217
+ for answer, params in q.response_validator.valid_examples:
218
+ for key, value in params.items():
219
+ setattr(q, key, value)
220
+ q._validate_answer(answer)
221
+ for answer, params, reason in q.response_validator.invalid_examples:
222
+ for key, value in params.items():
223
+ setattr(q, key, value)
224
+ try:
225
+ q._validate_answer(answer)
226
+ except QuestionAnswerValidationError:
227
+ pass
228
+ else:
229
+ raise ValueError(f"Example {answer} should have failed for {reason}.")
230
+
231
+ @property
232
+ def new_default_instructions(self) -> "Prompt":
233
+ "This is set up as a property because there are mutable question values that determine how it is rendered."
234
+ return self.question_presentation + self.answering_instructions
235
+
236
+ @property
237
+ def parameters(self) -> set[str]:
238
+ """Return the parameters of the question."""
239
+ from jinja2 import Environment, meta
240
+
241
+ env = Environment()
242
+ # Parse the template
243
+ txt = self._all_text()
244
+ # txt = self.question_text
245
+ # if hasattr(self, "question_options"):
246
+ # txt += " ".join(self.question_options)
247
+ parsed_content = env.parse(txt)
248
+ # Extract undeclared variables
249
+ variables = meta.find_undeclared_variables(parsed_content)
250
+ # Return as a list
251
+ return set(variables)
252
+
253
+ def get_instructions(self, model: Optional[str] = None) -> type["PromptBase"]:
254
+ """Get the mathcing question-answering instructions for the question.
255
+
256
+ :param model: The language model to use.
257
+ """
258
+ from edsl.prompts.Prompt import Prompt
259
+
260
+ if model in self.model_instructions:
261
+ return Prompt(text=self.model_instructions[model])
262
+ else:
263
+ if hasattr(self, "new_default_instructions"):
264
+ return self.new_default_instructions
265
+ else:
266
+ return self.applicable_prompts(model)[0]()
@@ -1,8 +1,60 @@
1
1
  from __future__ import annotations
2
- import random
3
- from typing import Any, Optional, Union
2
+ from typing import Any, Optional, Union, List
3
+
4
+ from pydantic import Field, BaseModel, validator
5
+
4
6
  from edsl.questions.QuestionBase import QuestionBase
5
7
  from edsl.questions.descriptors import IntegerDescriptor, QuestionOptionsDescriptor
8
+ from edsl.questions.ResponseValidatorABC import ResponseValidatorABC
9
+
10
+
11
+ class BudgewResponseValidator(ResponseValidatorABC):
12
+ valid_examples = []
13
+
14
+ invalid_examples = []
15
+
16
+ def fix(self, response, verbose=False):
17
+ if verbose:
18
+ print(f"Fixing list response: {response}")
19
+ answer = str(response.get("answer") or response.get("generated_tokens", ""))
20
+ if len(answer.split(",")) > 0:
21
+ return (
22
+ {"answer": answer.split(",")} | {"comment": response.get("comment")}
23
+ if "comment" in response
24
+ else {}
25
+ )
26
+
27
+
28
+ def create_budget_model(
29
+ budget_sum: float, permissive: bool, question_options: List[str]
30
+ ):
31
+ class BudgetResponse(BaseModel):
32
+ answer: List[float] = Field(
33
+ ...,
34
+ description="List of non-negative numbers representing budget allocation",
35
+ min_items=len(question_options),
36
+ max_items=len(question_options),
37
+ )
38
+ comment: Optional[str] = None
39
+ generated_tokens: Optional[str] = None
40
+
41
+ @validator("answer")
42
+ def validate_answer(cls, v):
43
+ if len(v) != len(question_options):
44
+ raise ValueError(f"Must provide {len(question_options)} values")
45
+ if any(x < 0 for x in v):
46
+ raise ValueError("All values must be non-negative")
47
+ total = sum(v)
48
+ if not permissive and total != budget_sum:
49
+ raise ValueError(f"Sum of numbers must equal {budget_sum}")
50
+ elif permissive and total > budget_sum:
51
+ raise ValueError(f"Sum of numbers cannot exceed {budget_sum}")
52
+ return v
53
+
54
+ class Config:
55
+ extra = "forbid"
56
+
57
+ return BudgetResponse
6
58
 
7
59
 
8
60
  class QuestionBudget(QuestionBase):
@@ -11,6 +63,8 @@ class QuestionBudget(QuestionBase):
11
63
  question_type = "budget"
12
64
  budget_sum: int = IntegerDescriptor(none_allowed=False)
13
65
  question_options: list[str] = QuestionOptionsDescriptor(q_budget=True)
66
+ _response_model = None
67
+ response_validator_class = BudgewResponseValidator
14
68
 
15
69
  def __init__(
16
70
  self,
@@ -18,6 +72,10 @@ class QuestionBudget(QuestionBase):
18
72
  question_text: str,
19
73
  question_options: list[str],
20
74
  budget_sum: int,
75
+ include_comment: bool = True,
76
+ question_presentation: Optional[str] = None,
77
+ answering_instructions: Optional[str] = None,
78
+ permissive: bool = False,
21
79
  ):
22
80
  """Instantiate a new QuestionBudget.
23
81
 
@@ -30,20 +88,19 @@ class QuestionBudget(QuestionBase):
30
88
  self.question_text = question_text
31
89
  self.question_options = question_options
32
90
  self.budget_sum = budget_sum
33
-
34
- ################
35
- # Answer methods
36
- ################
37
- def _validate_answer(self, answer: dict[str, Any]) -> dict[str, Union[int, str]]:
38
- """Validate the answer."""
39
- self._validate_answer_template_basic(answer)
40
- self._validate_answer_key_value(answer, "answer", dict)
41
- self._validate_answer_budget(answer)
42
- return answer
91
+ self.question_presentation = question_presentation
92
+ self.answering_instructions = answering_instructions
93
+ self.permissive = permissive
94
+ self.include_comment = include_comment
95
+
96
+ def create_response_model(self):
97
+ return create_budget_model(
98
+ self.budget_sum, self.permissive, self.question_options
99
+ )
43
100
 
44
101
  def _translate_answer_code_to_answer(
45
- self, answer_codes: dict[str, int], scenario: "Scenario" = None
46
- ):
102
+ self, answer_code, combined_dict
103
+ ) -> list[dict]:
47
104
  """
48
105
  Translate the answer codes to the actual answers.
49
106
 
@@ -52,35 +109,35 @@ class QuestionBudget(QuestionBase):
52
109
  This code will translate that to "a".
53
110
  """
54
111
  translated_codes = []
55
- for answer_code, response in answer_codes.items():
56
- translated_codes.append({self.question_options[int(answer_code)]: response})
112
+ for answer_code, question_option in zip(answer_code, self.question_options):
113
+ translated_codes.append({question_option: answer_code})
57
114
 
58
115
  return translated_codes
59
116
 
60
- def _simulate_answer(self, human_readable=True):
61
- """Simulate a valid answer for debugging purposes (what the validator expects)."""
62
- from edsl.utilities.utilities import random_string
63
-
64
- if human_readable:
65
- keys = self.question_options
66
- else:
67
- keys = range(len(self.question_options))
68
- remaining_budget = self.budget_sum
69
- values = []
70
- for _ in range(len(self.question_options)):
71
- if _ == len(self.question_options) - 1:
72
- # Assign remaining budget to the last value
73
- values.append(remaining_budget)
74
- else:
75
- # Generate a random value between 0 and remaining budget
76
- value = random.randint(0, remaining_budget)
77
- values.append(value)
78
- remaining_budget -= value
79
- answer = dict(zip(keys, values))
80
- return {
81
- "answer": answer,
82
- "comment": random_string(),
83
- }
117
+ # def _simulate_answer(self, human_readable=True):
118
+ # """Simulate a valid answer for debugging purposes (what the validator expects)."""
119
+ # from edsl.utilities.utilities import random_string
120
+
121
+ # if human_readable:
122
+ # keys = self.question_options
123
+ # else:
124
+ # keys = range(len(self.question_options))
125
+ # remaining_budget = self.budget_sum
126
+ # values = []
127
+ # for _ in range(len(self.question_options)):
128
+ # if _ == len(self.question_options) - 1:
129
+ # # Assign remaining budget to the last value
130
+ # values.append(remaining_budget)
131
+ # else:
132
+ # # Generate a random value between 0 and remaining budget
133
+ # value = random.randint(0, remaining_budget)
134
+ # values.append(value)
135
+ # remaining_budget -= value
136
+ # answer = dict(zip(keys, values))
137
+ # return {
138
+ # "answer": answer,
139
+ # "comment": random_string(),
140
+ # }
84
141
 
85
142
  @property
86
143
  def question_html_content(self) -> str:
@@ -127,13 +184,14 @@ class QuestionBudget(QuestionBase):
127
184
  # Helpful methods
128
185
  ################
129
186
  @classmethod
130
- def example(cls) -> QuestionBudget:
187
+ def example(cls, include_comment: bool = True) -> QuestionBudget:
131
188
  """Return an example of a budget question."""
132
189
  return cls(
133
190
  question_name="food_budget",
134
191
  question_text="How would you allocate $100?",
135
192
  question_options=["Pizza", "Ice Cream", "Burgers", "Salad"],
136
193
  budget_sum=100,
194
+ include_comment=include_comment,
137
195
  )
138
196
 
139
197