edsl 0.1.31.dev4__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 (188) 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 +136 -221
  8. edsl/agents/InvigilatorBase.py +148 -59
  9. edsl/agents/{PromptConstructionMixin.py → PromptConstructor.py} +154 -85
  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 +48 -47
  23. edsl/conjure/Conjure.py +6 -0
  24. edsl/coop/PriceFetcher.py +58 -0
  25. edsl/coop/coop.py +50 -7
  26. edsl/data/Cache.py +35 -1
  27. edsl/data/CacheHandler.py +3 -4
  28. edsl/data_transfer_models.py +73 -38
  29. edsl/enums.py +8 -0
  30. edsl/exceptions/general.py +10 -8
  31. edsl/exceptions/language_models.py +25 -1
  32. edsl/exceptions/questions.py +62 -5
  33. edsl/exceptions/results.py +4 -0
  34. edsl/inference_services/AnthropicService.py +13 -11
  35. edsl/inference_services/AwsBedrock.py +112 -0
  36. edsl/inference_services/AzureAI.py +214 -0
  37. edsl/inference_services/DeepInfraService.py +4 -3
  38. edsl/inference_services/GoogleService.py +16 -12
  39. edsl/inference_services/GroqService.py +5 -4
  40. edsl/inference_services/InferenceServiceABC.py +58 -3
  41. edsl/inference_services/InferenceServicesCollection.py +13 -8
  42. edsl/inference_services/MistralAIService.py +120 -0
  43. edsl/inference_services/OllamaService.py +18 -0
  44. edsl/inference_services/OpenAIService.py +55 -56
  45. edsl/inference_services/TestService.py +80 -0
  46. edsl/inference_services/TogetherAIService.py +170 -0
  47. edsl/inference_services/models_available_cache.py +25 -0
  48. edsl/inference_services/registry.py +19 -1
  49. edsl/jobs/Answers.py +10 -12
  50. edsl/jobs/FailedQuestion.py +78 -0
  51. edsl/jobs/Jobs.py +137 -41
  52. edsl/jobs/buckets/BucketCollection.py +24 -15
  53. edsl/jobs/buckets/TokenBucket.py +105 -18
  54. edsl/jobs/interviews/Interview.py +393 -83
  55. edsl/jobs/interviews/{interview_exception_tracking.py → InterviewExceptionCollection.py} +22 -18
  56. edsl/jobs/interviews/InterviewExceptionEntry.py +167 -0
  57. edsl/jobs/runners/JobsRunnerAsyncio.py +152 -160
  58. edsl/jobs/runners/JobsRunnerStatus.py +331 -0
  59. edsl/jobs/tasks/QuestionTaskCreator.py +30 -23
  60. edsl/jobs/tasks/TaskCreators.py +1 -1
  61. edsl/jobs/tasks/TaskHistory.py +205 -126
  62. edsl/language_models/LanguageModel.py +297 -177
  63. edsl/language_models/ModelList.py +2 -2
  64. edsl/language_models/RegisterLanguageModelsMeta.py +14 -29
  65. edsl/language_models/fake_openai_call.py +15 -0
  66. edsl/language_models/fake_openai_service.py +61 -0
  67. edsl/language_models/registry.py +25 -8
  68. edsl/language_models/repair.py +0 -19
  69. edsl/language_models/utilities.py +61 -0
  70. edsl/notebooks/Notebook.py +20 -2
  71. edsl/prompts/Prompt.py +52 -2
  72. edsl/questions/AnswerValidatorMixin.py +23 -26
  73. edsl/questions/QuestionBase.py +330 -249
  74. edsl/questions/QuestionBaseGenMixin.py +133 -0
  75. edsl/questions/QuestionBasePromptsMixin.py +266 -0
  76. edsl/questions/QuestionBudget.py +99 -42
  77. edsl/questions/QuestionCheckBox.py +227 -36
  78. edsl/questions/QuestionExtract.py +98 -28
  79. edsl/questions/QuestionFreeText.py +47 -31
  80. edsl/questions/QuestionFunctional.py +7 -0
  81. edsl/questions/QuestionList.py +141 -23
  82. edsl/questions/QuestionMultipleChoice.py +159 -66
  83. edsl/questions/QuestionNumerical.py +88 -47
  84. edsl/questions/QuestionRank.py +182 -25
  85. edsl/questions/Quick.py +41 -0
  86. edsl/questions/RegisterQuestionsMeta.py +31 -12
  87. edsl/questions/ResponseValidatorABC.py +170 -0
  88. edsl/questions/__init__.py +3 -4
  89. edsl/questions/decorators.py +21 -0
  90. edsl/questions/derived/QuestionLikertFive.py +10 -5
  91. edsl/questions/derived/QuestionLinearScale.py +15 -2
  92. edsl/questions/derived/QuestionTopK.py +10 -1
  93. edsl/questions/derived/QuestionYesNo.py +24 -3
  94. edsl/questions/descriptors.py +43 -7
  95. edsl/questions/prompt_templates/question_budget.jinja +13 -0
  96. edsl/questions/prompt_templates/question_checkbox.jinja +32 -0
  97. edsl/questions/prompt_templates/question_extract.jinja +11 -0
  98. edsl/questions/prompt_templates/question_free_text.jinja +3 -0
  99. edsl/questions/prompt_templates/question_linear_scale.jinja +11 -0
  100. edsl/questions/prompt_templates/question_list.jinja +17 -0
  101. edsl/questions/prompt_templates/question_multiple_choice.jinja +33 -0
  102. edsl/questions/prompt_templates/question_numerical.jinja +37 -0
  103. edsl/questions/question_registry.py +6 -2
  104. edsl/questions/templates/__init__.py +0 -0
  105. edsl/questions/templates/budget/__init__.py +0 -0
  106. edsl/questions/templates/budget/answering_instructions.jinja +7 -0
  107. edsl/questions/templates/budget/question_presentation.jinja +7 -0
  108. edsl/questions/templates/checkbox/__init__.py +0 -0
  109. edsl/questions/templates/checkbox/answering_instructions.jinja +10 -0
  110. edsl/questions/templates/checkbox/question_presentation.jinja +22 -0
  111. edsl/questions/templates/extract/__init__.py +0 -0
  112. edsl/questions/templates/extract/answering_instructions.jinja +7 -0
  113. edsl/questions/templates/extract/question_presentation.jinja +1 -0
  114. edsl/questions/templates/free_text/__init__.py +0 -0
  115. edsl/questions/templates/free_text/answering_instructions.jinja +0 -0
  116. edsl/questions/templates/free_text/question_presentation.jinja +1 -0
  117. edsl/questions/templates/likert_five/__init__.py +0 -0
  118. edsl/questions/templates/likert_five/answering_instructions.jinja +10 -0
  119. edsl/questions/templates/likert_five/question_presentation.jinja +12 -0
  120. edsl/questions/templates/linear_scale/__init__.py +0 -0
  121. edsl/questions/templates/linear_scale/answering_instructions.jinja +5 -0
  122. edsl/questions/templates/linear_scale/question_presentation.jinja +5 -0
  123. edsl/questions/templates/list/__init__.py +0 -0
  124. edsl/questions/templates/list/answering_instructions.jinja +4 -0
  125. edsl/questions/templates/list/question_presentation.jinja +5 -0
  126. edsl/questions/templates/multiple_choice/__init__.py +0 -0
  127. edsl/questions/templates/multiple_choice/answering_instructions.jinja +9 -0
  128. edsl/questions/templates/multiple_choice/html.jinja +0 -0
  129. edsl/questions/templates/multiple_choice/question_presentation.jinja +12 -0
  130. edsl/questions/templates/numerical/__init__.py +0 -0
  131. edsl/questions/templates/numerical/answering_instructions.jinja +8 -0
  132. edsl/questions/templates/numerical/question_presentation.jinja +7 -0
  133. edsl/questions/templates/rank/__init__.py +0 -0
  134. edsl/questions/templates/rank/answering_instructions.jinja +11 -0
  135. edsl/questions/templates/rank/question_presentation.jinja +15 -0
  136. edsl/questions/templates/top_k/__init__.py +0 -0
  137. edsl/questions/templates/top_k/answering_instructions.jinja +8 -0
  138. edsl/questions/templates/top_k/question_presentation.jinja +22 -0
  139. edsl/questions/templates/yes_no/__init__.py +0 -0
  140. edsl/questions/templates/yes_no/answering_instructions.jinja +6 -0
  141. edsl/questions/templates/yes_no/question_presentation.jinja +12 -0
  142. edsl/results/Dataset.py +20 -0
  143. edsl/results/DatasetExportMixin.py +58 -30
  144. edsl/results/DatasetTree.py +145 -0
  145. edsl/results/Result.py +32 -5
  146. edsl/results/Results.py +135 -46
  147. edsl/results/ResultsDBMixin.py +3 -3
  148. edsl/results/Selector.py +118 -0
  149. edsl/results/tree_explore.py +115 -0
  150. edsl/scenarios/FileStore.py +71 -10
  151. edsl/scenarios/Scenario.py +109 -24
  152. edsl/scenarios/ScenarioImageMixin.py +2 -2
  153. edsl/scenarios/ScenarioList.py +546 -21
  154. edsl/scenarios/ScenarioListExportMixin.py +24 -4
  155. edsl/scenarios/ScenarioListPdfMixin.py +153 -4
  156. edsl/study/SnapShot.py +8 -1
  157. edsl/study/Study.py +32 -0
  158. edsl/surveys/Rule.py +15 -3
  159. edsl/surveys/RuleCollection.py +21 -5
  160. edsl/surveys/Survey.py +707 -298
  161. edsl/surveys/SurveyExportMixin.py +71 -9
  162. edsl/surveys/SurveyFlowVisualizationMixin.py +2 -1
  163. edsl/surveys/SurveyQualtricsImport.py +284 -0
  164. edsl/surveys/instructions/ChangeInstruction.py +47 -0
  165. edsl/surveys/instructions/Instruction.py +34 -0
  166. edsl/surveys/instructions/InstructionCollection.py +77 -0
  167. edsl/surveys/instructions/__init__.py +0 -0
  168. edsl/templates/error_reporting/base.html +24 -0
  169. edsl/templates/error_reporting/exceptions_by_model.html +35 -0
  170. edsl/templates/error_reporting/exceptions_by_question_name.html +17 -0
  171. edsl/templates/error_reporting/exceptions_by_type.html +17 -0
  172. edsl/templates/error_reporting/interview_details.html +116 -0
  173. edsl/templates/error_reporting/interviews.html +10 -0
  174. edsl/templates/error_reporting/overview.html +5 -0
  175. edsl/templates/error_reporting/performance_plot.html +2 -0
  176. edsl/templates/error_reporting/report.css +74 -0
  177. edsl/templates/error_reporting/report.html +118 -0
  178. edsl/templates/error_reporting/report.js +25 -0
  179. edsl/utilities/utilities.py +40 -1
  180. {edsl-0.1.31.dev4.dist-info → edsl-0.1.33.dist-info}/METADATA +8 -2
  181. edsl-0.1.33.dist-info/RECORD +295 -0
  182. edsl/jobs/interviews/InterviewTaskBuildingMixin.py +0 -271
  183. edsl/jobs/interviews/retry_management.py +0 -37
  184. edsl/jobs/runners/JobsRunnerStatusMixin.py +0 -303
  185. edsl/utilities/gcp_bucket/simple_example.py +0 -9
  186. edsl-0.1.31.dev4.dist-info/RECORD +0 -204
  187. {edsl-0.1.31.dev4.dist-info → edsl-0.1.33.dist-info}/LICENSE +0 -0
  188. {edsl-0.1.31.dev4.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
 
@@ -25,26 +83,24 @@ class QuestionBudget(QuestionBase):
25
83
  :param question_text: The text of the question.
26
84
  :param question_options: The options for allocation of the budget sum.
27
85
  :param budget_sum: The total amount of the budget to be allocated among the options.
28
- :param instructions: Instructions for the question. If not provided, the default instructions are used. To view them, run `QuestionBudget.default_instructions`.
29
86
  """
30
87
  self.question_name = question_name
31
88
  self.question_text = question_text
32
89
  self.question_options = question_options
33
90
  self.budget_sum = budget_sum
34
-
35
- ################
36
- # Answer methods
37
- ################
38
- def _validate_answer(self, answer: dict[str, Any]) -> dict[str, Union[int, str]]:
39
- """Validate the answer."""
40
- self._validate_answer_template_basic(answer)
41
- self._validate_answer_key_value(answer, "answer", dict)
42
- self._validate_answer_budget(answer)
43
- 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
+ )
44
100
 
45
101
  def _translate_answer_code_to_answer(
46
- self, answer_codes: dict[str, int], scenario: "Scenario" = None
47
- ):
102
+ self, answer_code, combined_dict
103
+ ) -> list[dict]:
48
104
  """
49
105
  Translate the answer codes to the actual answers.
50
106
 
@@ -53,35 +109,35 @@ class QuestionBudget(QuestionBase):
53
109
  This code will translate that to "a".
54
110
  """
55
111
  translated_codes = []
56
- for answer_code, response in answer_codes.items():
57
- 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})
58
114
 
59
115
  return translated_codes
60
116
 
61
- def _simulate_answer(self, human_readable=True):
62
- """Simulate a valid answer for debugging purposes (what the validator expects)."""
63
- from edsl.utilities.utilities import random_string
64
-
65
- if human_readable:
66
- keys = self.question_options
67
- else:
68
- keys = range(len(self.question_options))
69
- remaining_budget = self.budget_sum
70
- values = []
71
- for _ in range(len(self.question_options)):
72
- if _ == len(self.question_options) - 1:
73
- # Assign remaining budget to the last value
74
- values.append(remaining_budget)
75
- else:
76
- # Generate a random value between 0 and remaining budget
77
- value = random.randint(0, remaining_budget)
78
- values.append(value)
79
- remaining_budget -= value
80
- answer = dict(zip(keys, values))
81
- return {
82
- "answer": answer,
83
- "comment": random_string(),
84
- }
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
+ # }
85
141
 
86
142
  @property
87
143
  def question_html_content(self) -> str:
@@ -128,13 +184,14 @@ class QuestionBudget(QuestionBase):
128
184
  # Helpful methods
129
185
  ################
130
186
  @classmethod
131
- def example(cls) -> QuestionBudget:
187
+ def example(cls, include_comment: bool = True) -> QuestionBudget:
132
188
  """Return an example of a budget question."""
133
189
  return cls(
134
190
  question_name="food_budget",
135
191
  question_text="How would you allocate $100?",
136
192
  question_options=["Pizza", "Ice Cream", "Burgers", "Salad"],
137
193
  budget_sum=100,
194
+ include_comment=include_comment,
138
195
  )
139
196
 
140
197