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
@@ -4,6 +4,123 @@ import textwrap
4
4
  from typing import Any, Optional, Union
5
5
  from edsl.questions.QuestionBase import QuestionBase
6
6
  from edsl.questions.descriptors import IntegerOrNoneDescriptor
7
+ from edsl.questions.decorators import inject_exception
8
+
9
+ from pydantic import field_validator, Field
10
+ from edsl.questions.ResponseValidatorABC import ResponseValidatorABC
11
+ from edsl.questions.ResponseValidatorABC import BaseResponse
12
+
13
+ from edsl.exceptions import QuestionAnswerValidationError
14
+ import textwrap
15
+ import json
16
+
17
+ from json_repair import repair_json
18
+
19
+
20
+ def convert_string(s):
21
+ """Convert a string to a more appropriate type if possible.
22
+
23
+ >>> convert_string("3.14")
24
+ 3.14
25
+ >>> convert_string("42")
26
+ 42
27
+ >>> convert_string("hello")
28
+ 'hello'
29
+ >>> convert_string('{"key": "value"}')
30
+ {'key': 'value'}
31
+ >>> convert_string("{'key': 'value'}")
32
+ {'key': 'value'}
33
+ """
34
+
35
+ if not isinstance(s, str): # if it's not a string, return it as is
36
+ return s
37
+
38
+ # If the repair returns, continue on; otherwise, try to load it as JSON
39
+ if (repaired_json := repair_json(s)) == '""':
40
+ pass
41
+ else:
42
+ try:
43
+ return json.loads(repaired_json)
44
+ except json.JSONDecodeError:
45
+ pass
46
+
47
+ # Try to convert to float
48
+ try:
49
+ return float(s)
50
+ except ValueError:
51
+ pass
52
+
53
+ # Try to convert to int
54
+ try:
55
+ return int(s)
56
+ except ValueError:
57
+ pass
58
+
59
+ # If all conversions fail, return the original string
60
+ return s
61
+
62
+
63
+ def create_model(max_list_items: int, permissive):
64
+ from pydantic import BaseModel
65
+
66
+ if permissive or max_list_items is None:
67
+
68
+ class ListResponse(BaseModel):
69
+ answer: list[Any]
70
+ comment: Optional[str] = None
71
+ generated_tokens: Optional[str] = None
72
+
73
+ else:
74
+
75
+ class ListResponse(BaseModel):
76
+ """
77
+ >>> nr = ListResponse(answer=["Apple", "Cherry"])
78
+ >>> nr.dict()
79
+ {'answer': ['Apple', 'Cherry'], 'comment': None, 'generated_tokens': None}
80
+ """
81
+
82
+ answer: list[Any] = Field(..., min_items=0, max_items=max_list_items)
83
+ comment: Optional[str] = None
84
+ generated_tokens: Optional[str] = None
85
+
86
+ return ListResponse
87
+
88
+
89
+ class ListResponseValidator(ResponseValidatorABC):
90
+ required_params = ["max_list_items", "permissive"]
91
+ valid_examples = [({"answer": ["hello", "world"]}, {"max_list_items": 5})]
92
+
93
+ invalid_examples = [
94
+ (
95
+ {"answer": ["hello", "world", "this", "is", "a", "test"]},
96
+ {"max_list_items": 5},
97
+ "Too many items.",
98
+ ),
99
+ ]
100
+
101
+ def _check_constraints(self, response) -> None:
102
+ if (
103
+ self.max_list_items is not None
104
+ and len(response.answer) > self.max_list_items
105
+ ):
106
+ raise QuestionAnswerValidationError("Too many items.")
107
+
108
+ def fix(self, response, verbose=False):
109
+ if verbose:
110
+ print(f"Fixing list response: {response}")
111
+ answer = str(response.get("answer") or response.get("generated_tokens", ""))
112
+ if len(answer.split(",")) > 0:
113
+ return (
114
+ {"answer": answer.split(",")} | {"comment": response.get("comment")}
115
+ if "comment" in response
116
+ else {}
117
+ )
118
+
119
+ def _post_process(self, edsl_answer_dict):
120
+ edsl_answer_dict["answer"] = [
121
+ convert_string(item) for item in edsl_answer_dict["answer"]
122
+ ]
123
+ return edsl_answer_dict
7
124
 
8
125
 
9
126
  class QuestionList(QuestionBase):
@@ -11,44 +128,38 @@ class QuestionList(QuestionBase):
11
128
 
12
129
  question_type = "list"
13
130
  max_list_items: int = IntegerOrNoneDescriptor()
131
+ _response_model = None
132
+ response_validator_class = ListResponseValidator
14
133
 
15
134
  def __init__(
16
135
  self,
17
136
  question_name: str,
18
137
  question_text: str,
19
138
  max_list_items: Optional[int] = None,
139
+ include_comment: bool = True,
140
+ answering_instructions: Optional[str] = None,
141
+ question_presentation: Optional[str] = None,
142
+ permissive: bool = False,
20
143
  ):
21
144
  """Instantiate a new QuestionList.
22
145
 
23
146
  :param question_name: The name of the question.
24
147
  :param question_text: The text of the question.
25
- :param instructions: Instructions for the question. If not provided, the default instructions are used. To view them, run `QuestionList.default_instructions`.
26
148
  :param max_list_items: The maximum number of items that can be in the answer list.
149
+
150
+ >>> QuestionList.example().self_check()
27
151
  """
28
152
  self.question_name = question_name
29
153
  self.question_text = question_text
30
154
  self.max_list_items = max_list_items
155
+ self.permissive = permissive
31
156
 
32
- ################
33
- # Answer methods
34
- ################
35
- def _validate_answer(self, answer: Any) -> dict[str, Union[list[str], str]]:
36
- """Validate the answer."""
37
- self._validate_answer_template_basic(answer)
38
- self._validate_answer_key_value(answer, "answer", list)
39
- self._validate_answer_list(answer)
40
- return answer
41
-
42
- def _translate_answer_code_to_answer(self, answer, scenario: "Scenario" = None):
43
- """There is no answer code."""
44
- return answer
45
-
46
- def _simulate_answer(self, human_readable: bool = True):
47
- """Simulate a valid answer for debugging purposes (what the validator expects)."""
48
- num_items = random.randint(1, self.max_list_items or 2)
49
- from edsl.utilities.utilities import random_string
157
+ self.include_comment = include_comment
158
+ self.answering_instructions = answering_instructions
159
+ self.question_presentations = question_presentation
50
160
 
51
- return {"answer": [random_string() for _ in range(num_items)]}
161
+ def create_response_model(self):
162
+ return create_model(self.max_list_items, self.permissive)
52
163
 
53
164
  @property
54
165
  def question_html_content(self) -> str:
@@ -79,12 +190,17 @@ class QuestionList(QuestionBase):
79
190
  # Helpful methods
80
191
  ################
81
192
  @classmethod
82
- def example(cls) -> QuestionList:
193
+ @inject_exception
194
+ def example(
195
+ cls, include_comment=True, max_list_items=None, permissive=False
196
+ ) -> QuestionList:
83
197
  """Return an example of a list question."""
84
198
  return cls(
85
199
  question_name="list_of_foods",
86
200
  question_text="What are your favorite foods?",
87
- max_list_items=5,
201
+ include_comment=include_comment,
202
+ max_list_items=max_list_items,
203
+ permissive=permissive,
88
204
  )
89
205
 
90
206
 
@@ -92,7 +208,7 @@ def main():
92
208
  """Create an example of a list question and demonstrate its functionality."""
93
209
  from edsl.questions.QuestionList import QuestionList
94
210
 
95
- q = QuestionList.example()
211
+ q = QuestionList.example(max_list_items=5)
96
212
  q.question_text
97
213
  q.question_name
98
214
  q.max_list_items
@@ -108,6 +224,8 @@ def main():
108
224
  q.to_dict()
109
225
  assert q.from_dict(q.to_dict()) == q
110
226
 
227
+
228
+ if __name__ == "__main__":
111
229
  import doctest
112
230
 
113
231
  doctest.testmod(optionflags=doctest.ELLIPSIS)
@@ -1,12 +1,114 @@
1
1
  from __future__ import annotations
2
- import time
3
- from typing import Union
4
- import random
5
- from typing import Optional
2
+ from typing import Union, Literal, Optional, List, Any
3
+
6
4
  from jinja2 import Template
5
+ from pydantic import BaseModel, Field
7
6
 
7
+ from edsl.scenarios.Scenario import Scenario
8
8
  from edsl.questions.QuestionBase import QuestionBase
9
9
  from edsl.questions.descriptors import QuestionOptionsDescriptor
10
+ from edsl.questions.decorators import inject_exception
11
+ from edsl.questions.ResponseValidatorABC import ResponseValidatorABC
12
+
13
+
14
+ def create_response_model(choices: List[str], permissive: bool = False):
15
+ """
16
+ Create a ChoiceResponse model class with a predefined list of choices.
17
+
18
+ :param choices: A list of allowed values for the answer field.
19
+ :param permissive: If True, any value will be accepted as an answer.
20
+ :return: A new Pydantic model class.
21
+ """
22
+ choice_tuple = tuple(choices)
23
+
24
+ if not permissive:
25
+
26
+ class ChoiceResponse(BaseModel):
27
+ answer: Literal[choice_tuple] = Field(description="Selected choice")
28
+ comment: Optional[str] = Field(None, description="Optional comment field")
29
+ generated_tokens: Optional[Any] = Field(
30
+ None, description="Generated tokens"
31
+ )
32
+
33
+ class Config:
34
+ @staticmethod
35
+ def json_schema_extra(schema: dict, model: BaseModel) -> None:
36
+ for prop in schema.get("properties", {}).values():
37
+ if prop.get("title") == "answer":
38
+ prop["enum"] = choices
39
+
40
+ else:
41
+
42
+ class ChoiceResponse(BaseModel):
43
+ answer: Any = Field(description="Selected choice (can be any value)")
44
+ comment: Optional[str] = Field(None, description="Optional comment field")
45
+ generated_tokens: Optional[Any] = Field(
46
+ None, description="Generated tokens"
47
+ )
48
+
49
+ class Config:
50
+ @staticmethod
51
+ def json_schema_extra(schema: dict, model: BaseModel) -> None:
52
+ for prop in schema.get("properties", {}).values():
53
+ if prop.get("title") == "answer":
54
+ prop["description"] += f". Suggested choices are: {choices}"
55
+ schema["title"] += " (Permissive)"
56
+
57
+ return ChoiceResponse
58
+
59
+
60
+ class MultipleChoiceResponseValidator(ResponseValidatorABC):
61
+ required_params = ["question_options", "use_code"]
62
+
63
+ def fix(self, response, verbose=False):
64
+ response_text = str(response.get("answer"))
65
+ if response_text is None:
66
+ response_text = response.get("generated_tokens", "")
67
+
68
+ if verbose:
69
+ print(f"Invalid generated tokens was: {response_text}")
70
+
71
+ matches = []
72
+ for idx, option in enumerate(self.question_options):
73
+ if verbose:
74
+ print("The options are: ", self.question_options)
75
+ if str(option) in response_text:
76
+ if verbose:
77
+ print("Match found with option ", option)
78
+ if option not in matches:
79
+ matches.append(option)
80
+
81
+ if verbose:
82
+ print("The matches are: ", matches)
83
+ if len(matches) == 1:
84
+ proposed_data = {
85
+ "answer": matches[0],
86
+ "generated_tokens": response.get("generated_tokens", None),
87
+ }
88
+ try:
89
+ self.response_model(**proposed_data)
90
+ return proposed_data
91
+ except Exception as e:
92
+ if verbose:
93
+ print(f"Proposed solution {proposed_data} is invalid. Error: {e}")
94
+ return response
95
+
96
+ valid_examples = [
97
+ ({"answer": 1}, {"question_options": ["Good", "Great", "OK", "Bad"]})
98
+ ]
99
+
100
+ invalid_examples = [
101
+ (
102
+ {"answer": -1},
103
+ {"question_options": ["Good", "Great", "OK", "Bad"]},
104
+ "Answer code must be a non-negative integer",
105
+ ),
106
+ (
107
+ {"answer": None},
108
+ {"question_options": ["Good", "Great", "OK", "Bad"]},
109
+ "Answer code must not be missing.",
110
+ ),
111
+ ]
10
112
 
11
113
 
12
114
  class QuestionMultipleChoice(QuestionBase):
@@ -21,50 +123,53 @@ class QuestionMultipleChoice(QuestionBase):
21
123
  question_options: Union[
22
124
  list[str], list[list], list[float], list[int]
23
125
  ] = QuestionOptionsDescriptor()
126
+ _response_model = None
127
+ response_validator_class = MultipleChoiceResponseValidator
24
128
 
25
129
  def __init__(
26
130
  self,
27
131
  question_name: str,
28
132
  question_text: str,
29
133
  question_options: Union[list[str], list[list], list[float], list[int]],
134
+ include_comment: bool = True,
135
+ use_code: bool = False,
136
+ answering_instructions: Optional[str] = None,
137
+ question_presentation: Optional[str] = None,
138
+ permissive: bool = False,
30
139
  ):
31
140
  """Instantiate a new QuestionMultipleChoice.
32
141
 
33
142
  :param question_name: The name of the question.
34
143
  :param question_text: The text of the question.
35
144
  :param question_options: The options the agent should select from.
36
- :param instructions: Instructions for the question. If not provided, the default instructions are used. To view them, run `QuestionMultipleChoice.default_instructions`.
145
+ :param include_comment: Whether to include a comment field.
146
+ :param use_code: Whether to use code for the options.
147
+ :param answering_instructions: Instructions for the question.
148
+ :param question_presentation: The presentation of the question.
149
+ :param permissive: Whether to force the answer to be one of the options.
150
+
37
151
  """
38
152
  self.question_name = question_name
39
153
  self.question_text = question_text
40
154
  self.question_options = question_options
41
155
 
42
- # @property
43
- # def question_options(self) -> Union[list[str], list[list], list[float], list[int]]:
44
- # """Return the question options."""
45
- # return self._question_options
156
+ self._include_comment = include_comment
157
+ self.use_code = use_code
158
+ self.answering_instructions = answering_instructions
159
+ self.question_presentation = question_presentation
160
+ self.permissive = permissive
46
161
 
47
162
  ################
48
163
  # Answer methods
49
164
  ################
50
- def _validate_answer(
51
- self, answer: dict[str, Union[str, int]]
52
- ) -> dict[str, Union[str, int]]:
53
- """Validate the answer.
54
165
 
55
- >>> q = QuestionMultipleChoice.example()
56
- >>> q._validate_answer({"answer": 0, "comment": "I like custard"})
57
- {'answer': 0, 'comment': 'I like custard'}
58
-
59
- >>> q = QuestionMultipleChoice(question_name="how_feeling", question_text="How are you?", question_options=["Good", "Great", "OK", "Bad"])
60
- >>> q._validate_answer({"answer": -1, "comment": "I like custard"})
61
- Traceback (most recent call last):
62
- ...
63
- edsl.exceptions.questions.QuestionAnswerValidationError: Answer code must be a non-negative integer (got -1).
64
- """
65
- self._validate_answer_template_basic(answer)
66
- self._validate_answer_multiple_choice(answer)
67
- return answer
166
+ def create_response_model(self):
167
+ if self.use_code:
168
+ return create_response_model(
169
+ list(range(len(self.question_options))), self.permissive
170
+ )
171
+ else:
172
+ return create_response_model(self.question_options, self.permissive)
68
173
 
69
174
  def _translate_answer_code_to_answer(
70
175
  self, answer_code: int, scenario: Optional["Scenario"] = None
@@ -75,15 +180,14 @@ class QuestionMultipleChoice(QuestionBase):
75
180
  The question options might be templates, so they need to be rendered with the scenario.
76
181
 
77
182
  >>> q = QuestionMultipleChoice.example()
78
- >>> q._translate_answer_code_to_answer(0, {})
183
+ >>> q._translate_answer_code_to_answer('Good', {})
79
184
  'Good'
80
185
 
81
186
  >>> q = QuestionMultipleChoice(question_name="how_feeling", question_text="How are you?", question_options=["{{emotion[0]}}", "emotion[1]"])
82
- >>> q._translate_answer_code_to_answer(0, {"emotion": ["Happy", "Sad"]})
187
+ >>> q._translate_answer_code_to_answer('Happy', {"emotion": ["Happy", "Sad"]})
83
188
  'Happy'
84
189
 
85
190
  """
86
- from edsl.scenarios.Scenario import Scenario
87
191
 
88
192
  scenario = scenario or Scenario()
89
193
 
@@ -96,31 +200,17 @@ class QuestionMultipleChoice(QuestionBase):
96
200
  question_option_key = list(meta.find_undeclared_variables(parsed_content))[
97
201
  0
98
202
  ]
99
- #breakpoint()
100
203
  translated_options = scenario.get(question_option_key)
101
204
  else:
102
205
  translated_options = [
103
206
  Template(str(option)).render(scenario)
104
207
  for option in self.question_options
105
208
  ]
106
- # print("Translated options:", translated_options)
107
- # breakpoint()
108
- return translated_options[int(answer_code)]
109
-
110
- def _simulate_answer(
111
- self, human_readable: bool = True
112
- ) -> dict[str, Union[int, str]]:
113
- """Simulate a valid answer for debugging purposes."""
114
- from edsl.utilities.utilities import random_string
115
-
116
- if human_readable:
117
- answer = random.choice(self.question_options)
209
+ if self._use_code:
210
+ return translated_options[int(answer_code)]
118
211
  else:
119
- answer = random.choice(range(len(self.question_options)))
120
- return {
121
- "answer": answer,
122
- "comment": random_string(),
123
- }
212
+ # return translated_options[answer_code]
213
+ return answer_code
124
214
 
125
215
  @property
126
216
  def question_html_content(self) -> str:
@@ -154,33 +244,36 @@ class QuestionMultipleChoice(QuestionBase):
154
244
  # Example
155
245
  ################
156
246
  @classmethod
157
- def example(cls) -> QuestionMultipleChoice:
247
+ @inject_exception
248
+ def example(cls, include_comment=False, use_code=False) -> QuestionMultipleChoice:
158
249
  """Return an example instance."""
159
250
  return cls(
160
251
  question_text="How are you?",
161
252
  question_options=["Good", "Great", "OK", "Bad"],
162
253
  question_name="how_feeling",
254
+ include_comment=include_comment,
255
+ use_code=use_code,
163
256
  )
164
257
 
165
258
 
166
- def main():
167
- """Create an example QuestionMultipleChoice and test its methods."""
168
- from edsl.questions.QuestionMultipleChoice import QuestionMultipleChoice
169
-
170
- q = QuestionMultipleChoice.example()
171
- q.question_text
172
- q.question_options
173
- q.question_name
174
- # validate an answer
175
- q._validate_answer({"answer": 0, "comment": "I like custard"})
176
- # translate answer code
177
- q._translate_answer_code_to_answer(0, {})
178
- # simulate answer
179
- q._simulate_answer()
180
- q._simulate_answer(human_readable=False)
181
- # serialization (inherits from Question)
182
- q.to_dict()
183
- assert q.from_dict(q.to_dict()) == q
259
+ # def main():
260
+ # """Create an example QuestionMultipleChoice and test its methods."""
261
+ # from edsl.questions.QuestionMultipleChoice import QuestionMultipleChoice
262
+
263
+ # q = QuestionMultipleChoice.example()
264
+ # q.question_text
265
+ # q.question_options
266
+ # q.question_name
267
+ # # validate an answer
268
+ # q._validate_answer({"answer": 0, "comment": "I like custard"})
269
+ # # translate answer code
270
+ # q._translate_answer_code_to_answer(0, {})
271
+ # # simulate answer
272
+ # q._simulate_answer()
273
+ # q._simulate_answer(human_readable=False)
274
+ # # serialization (inherits from Question)
275
+ # q.to_dict()
276
+ # assert q.from_dict(q.to_dict()) == q
184
277
 
185
278
 
186
279
  if __name__ == "__main__":