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
@@ -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,43 +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
148
  :param max_list_items: The maximum number of items that can be in the answer list.
149
+
150
+ >>> QuestionList.example().self_check()
26
151
  """
27
152
  self.question_name = question_name
28
153
  self.question_text = question_text
29
154
  self.max_list_items = max_list_items
155
+ self.permissive = permissive
30
156
 
31
- ################
32
- # Answer methods
33
- ################
34
- def _validate_answer(self, answer: Any) -> dict[str, Union[list[str], str]]:
35
- """Validate the answer."""
36
- self._validate_answer_template_basic(answer)
37
- self._validate_answer_key_value(answer, "answer", list)
38
- self._validate_answer_list(answer)
39
- return answer
40
-
41
- def _translate_answer_code_to_answer(self, answer, scenario: "Scenario" = None):
42
- """There is no answer code."""
43
- return answer
44
-
45
- def _simulate_answer(self, human_readable: bool = True):
46
- """Simulate a valid answer for debugging purposes (what the validator expects)."""
47
- num_items = random.randint(1, self.max_list_items or 2)
48
- 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
49
160
 
50
- 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)
51
163
 
52
164
  @property
53
165
  def question_html_content(self) -> str:
@@ -78,12 +190,17 @@ class QuestionList(QuestionBase):
78
190
  # Helpful methods
79
191
  ################
80
192
  @classmethod
81
- def example(cls) -> QuestionList:
193
+ @inject_exception
194
+ def example(
195
+ cls, include_comment=True, max_list_items=None, permissive=False
196
+ ) -> QuestionList:
82
197
  """Return an example of a list question."""
83
198
  return cls(
84
199
  question_name="list_of_foods",
85
200
  question_text="What are your favorite foods?",
86
- max_list_items=5,
201
+ include_comment=include_comment,
202
+ max_list_items=max_list_items,
203
+ permissive=permissive,
87
204
  )
88
205
 
89
206
 
@@ -91,7 +208,7 @@ def main():
91
208
  """Create an example of a list question and demonstrate its functionality."""
92
209
  from edsl.questions.QuestionList import QuestionList
93
210
 
94
- q = QuestionList.example()
211
+ q = QuestionList.example(max_list_items=5)
95
212
  q.question_text
96
213
  q.question_name
97
214
  q.max_list_items
@@ -107,6 +224,8 @@ def main():
107
224
  q.to_dict()
108
225
  assert q.from_dict(q.to_dict()) == q
109
226
 
227
+
228
+ if __name__ == "__main__":
110
229
  import doctest
111
230
 
112
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,49 +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.
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
+
36
151
  """
37
152
  self.question_name = question_name
38
153
  self.question_text = question_text
39
154
  self.question_options = question_options
40
155
 
41
- # @property
42
- # def question_options(self) -> Union[list[str], list[list], list[float], list[int]]:
43
- # """Return the question options."""
44
- # 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
45
161
 
46
162
  ################
47
163
  # Answer methods
48
164
  ################
49
- def _validate_answer(
50
- self, answer: dict[str, Union[str, int]]
51
- ) -> dict[str, Union[str, int]]:
52
- """Validate the answer.
53
165
 
54
- >>> q = QuestionMultipleChoice.example()
55
- >>> q._validate_answer({"answer": 0, "comment": "I like custard"})
56
- {'answer': 0, 'comment': 'I like custard'}
57
-
58
- >>> q = QuestionMultipleChoice(question_name="how_feeling", question_text="How are you?", question_options=["Good", "Great", "OK", "Bad"])
59
- >>> q._validate_answer({"answer": -1, "comment": "I like custard"})
60
- Traceback (most recent call last):
61
- ...
62
- edsl.exceptions.questions.QuestionAnswerValidationError: Answer code must be a non-negative integer (got -1).
63
- """
64
- self._validate_answer_template_basic(answer)
65
- self._validate_answer_multiple_choice(answer)
66
- 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)
67
173
 
68
174
  def _translate_answer_code_to_answer(
69
175
  self, answer_code: int, scenario: Optional["Scenario"] = None
@@ -74,15 +180,14 @@ class QuestionMultipleChoice(QuestionBase):
74
180
  The question options might be templates, so they need to be rendered with the scenario.
75
181
 
76
182
  >>> q = QuestionMultipleChoice.example()
77
- >>> q._translate_answer_code_to_answer(0, {})
183
+ >>> q._translate_answer_code_to_answer('Good', {})
78
184
  'Good'
79
185
 
80
186
  >>> q = QuestionMultipleChoice(question_name="how_feeling", question_text="How are you?", question_options=["{{emotion[0]}}", "emotion[1]"])
81
- >>> q._translate_answer_code_to_answer(0, {"emotion": ["Happy", "Sad"]})
187
+ >>> q._translate_answer_code_to_answer('Happy', {"emotion": ["Happy", "Sad"]})
82
188
  'Happy'
83
189
 
84
190
  """
85
- from edsl.scenarios.Scenario import Scenario
86
191
 
87
192
  scenario = scenario or Scenario()
88
193
 
@@ -95,31 +200,17 @@ class QuestionMultipleChoice(QuestionBase):
95
200
  question_option_key = list(meta.find_undeclared_variables(parsed_content))[
96
201
  0
97
202
  ]
98
- # breakpoint()
99
203
  translated_options = scenario.get(question_option_key)
100
204
  else:
101
205
  translated_options = [
102
206
  Template(str(option)).render(scenario)
103
207
  for option in self.question_options
104
208
  ]
105
- # print("Translated options:", translated_options)
106
- # breakpoint()
107
- return translated_options[int(answer_code)]
108
-
109
- def _simulate_answer(
110
- self, human_readable: bool = True
111
- ) -> dict[str, Union[int, str]]:
112
- """Simulate a valid answer for debugging purposes."""
113
- from edsl.utilities.utilities import random_string
114
-
115
- if human_readable:
116
- answer = random.choice(self.question_options)
209
+ if self._use_code:
210
+ return translated_options[int(answer_code)]
117
211
  else:
118
- answer = random.choice(range(len(self.question_options)))
119
- return {
120
- "answer": answer,
121
- "comment": random_string(),
122
- }
212
+ # return translated_options[answer_code]
213
+ return answer_code
123
214
 
124
215
  @property
125
216
  def question_html_content(self) -> str:
@@ -153,33 +244,36 @@ class QuestionMultipleChoice(QuestionBase):
153
244
  # Example
154
245
  ################
155
246
  @classmethod
156
- def example(cls) -> QuestionMultipleChoice:
247
+ @inject_exception
248
+ def example(cls, include_comment=False, use_code=False) -> QuestionMultipleChoice:
157
249
  """Return an example instance."""
158
250
  return cls(
159
251
  question_text="How are you?",
160
252
  question_options=["Good", "Great", "OK", "Bad"],
161
253
  question_name="how_feeling",
254
+ include_comment=include_comment,
255
+ use_code=use_code,
162
256
  )
163
257
 
164
258
 
165
- def main():
166
- """Create an example QuestionMultipleChoice and test its methods."""
167
- from edsl.questions.QuestionMultipleChoice import QuestionMultipleChoice
168
-
169
- q = QuestionMultipleChoice.example()
170
- q.question_text
171
- q.question_options
172
- q.question_name
173
- # validate an answer
174
- q._validate_answer({"answer": 0, "comment": "I like custard"})
175
- # translate answer code
176
- q._translate_answer_code_to_answer(0, {})
177
- # simulate answer
178
- q._simulate_answer()
179
- q._simulate_answer(human_readable=False)
180
- # serialization (inherits from Question)
181
- q.to_dict()
182
- 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
183
277
 
184
278
 
185
279
  if __name__ == "__main__":