edsl 0.1.33.dev1__py3-none-any.whl → 0.1.33.dev2__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 (163) hide show
  1. edsl/TemplateLoader.py +24 -0
  2. edsl/__init__.py +8 -4
  3. edsl/agents/Agent.py +46 -14
  4. edsl/agents/AgentList.py +43 -0
  5. edsl/agents/Invigilator.py +125 -212
  6. edsl/agents/InvigilatorBase.py +140 -32
  7. edsl/agents/PromptConstructionMixin.py +43 -66
  8. edsl/agents/__init__.py +1 -0
  9. edsl/auto/AutoStudy.py +117 -0
  10. edsl/auto/StageBase.py +230 -0
  11. edsl/auto/StageGenerateSurvey.py +178 -0
  12. edsl/auto/StageLabelQuestions.py +125 -0
  13. edsl/auto/StagePersona.py +61 -0
  14. edsl/auto/StagePersonaDimensionValueRanges.py +88 -0
  15. edsl/auto/StagePersonaDimensionValues.py +74 -0
  16. edsl/auto/StagePersonaDimensions.py +69 -0
  17. edsl/auto/StageQuestions.py +73 -0
  18. edsl/auto/SurveyCreatorPipeline.py +21 -0
  19. edsl/auto/utilities.py +224 -0
  20. edsl/config.py +38 -39
  21. edsl/coop/PriceFetcher.py +58 -0
  22. edsl/coop/coop.py +39 -5
  23. edsl/data/Cache.py +35 -1
  24. edsl/data_transfer_models.py +120 -38
  25. edsl/enums.py +2 -0
  26. edsl/exceptions/language_models.py +25 -1
  27. edsl/exceptions/questions.py +62 -5
  28. edsl/exceptions/results.py +4 -0
  29. edsl/inference_services/AnthropicService.py +13 -11
  30. edsl/inference_services/AwsBedrock.py +19 -17
  31. edsl/inference_services/AzureAI.py +37 -20
  32. edsl/inference_services/GoogleService.py +16 -12
  33. edsl/inference_services/GroqService.py +2 -0
  34. edsl/inference_services/InferenceServiceABC.py +24 -0
  35. edsl/inference_services/MistralAIService.py +120 -0
  36. edsl/inference_services/OpenAIService.py +41 -50
  37. edsl/inference_services/TestService.py +71 -0
  38. edsl/inference_services/models_available_cache.py +0 -6
  39. edsl/inference_services/registry.py +4 -0
  40. edsl/jobs/Answers.py +10 -12
  41. edsl/jobs/FailedQuestion.py +78 -0
  42. edsl/jobs/Jobs.py +18 -13
  43. edsl/jobs/buckets/TokenBucket.py +39 -14
  44. edsl/jobs/interviews/Interview.py +297 -77
  45. edsl/jobs/interviews/InterviewExceptionEntry.py +83 -19
  46. edsl/jobs/interviews/interview_exception_tracking.py +0 -70
  47. edsl/jobs/interviews/retry_management.py +3 -1
  48. edsl/jobs/runners/JobsRunnerAsyncio.py +116 -70
  49. edsl/jobs/runners/JobsRunnerStatusMixin.py +1 -1
  50. edsl/jobs/tasks/QuestionTaskCreator.py +30 -23
  51. edsl/jobs/tasks/TaskHistory.py +131 -213
  52. edsl/language_models/LanguageModel.py +239 -129
  53. edsl/language_models/ModelList.py +2 -2
  54. edsl/language_models/RegisterLanguageModelsMeta.py +14 -29
  55. edsl/language_models/fake_openai_call.py +15 -0
  56. edsl/language_models/fake_openai_service.py +61 -0
  57. edsl/language_models/registry.py +15 -2
  58. edsl/language_models/repair.py +0 -19
  59. edsl/language_models/utilities.py +61 -0
  60. edsl/prompts/Prompt.py +52 -2
  61. edsl/questions/AnswerValidatorMixin.py +23 -26
  62. edsl/questions/QuestionBase.py +273 -242
  63. edsl/questions/QuestionBaseGenMixin.py +133 -0
  64. edsl/questions/QuestionBasePromptsMixin.py +266 -0
  65. edsl/questions/QuestionBudget.py +6 -0
  66. edsl/questions/QuestionCheckBox.py +227 -35
  67. edsl/questions/QuestionExtract.py +98 -27
  68. edsl/questions/QuestionFreeText.py +46 -29
  69. edsl/questions/QuestionFunctional.py +7 -0
  70. edsl/questions/QuestionList.py +141 -22
  71. edsl/questions/QuestionMultipleChoice.py +173 -64
  72. edsl/questions/QuestionNumerical.py +87 -46
  73. edsl/questions/QuestionRank.py +182 -24
  74. edsl/questions/RegisterQuestionsMeta.py +31 -12
  75. edsl/questions/ResponseValidatorABC.py +169 -0
  76. edsl/questions/__init__.py +3 -4
  77. edsl/questions/decorators.py +21 -0
  78. edsl/questions/derived/QuestionLikertFive.py +10 -5
  79. edsl/questions/derived/QuestionLinearScale.py +11 -1
  80. edsl/questions/derived/QuestionTopK.py +6 -0
  81. edsl/questions/derived/QuestionYesNo.py +16 -1
  82. edsl/questions/descriptors.py +43 -7
  83. edsl/questions/prompt_templates/question_budget.jinja +13 -0
  84. edsl/questions/prompt_templates/question_checkbox.jinja +32 -0
  85. edsl/questions/prompt_templates/question_extract.jinja +11 -0
  86. edsl/questions/prompt_templates/question_free_text.jinja +3 -0
  87. edsl/questions/prompt_templates/question_linear_scale.jinja +11 -0
  88. edsl/questions/prompt_templates/question_list.jinja +17 -0
  89. edsl/questions/prompt_templates/question_multiple_choice.jinja +33 -0
  90. edsl/questions/prompt_templates/question_numerical.jinja +37 -0
  91. edsl/questions/question_registry.py +6 -2
  92. edsl/questions/templates/__init__.py +0 -0
  93. edsl/questions/templates/checkbox/__init__.py +0 -0
  94. edsl/questions/templates/checkbox/answering_instructions.jinja +10 -0
  95. edsl/questions/templates/checkbox/question_presentation.jinja +22 -0
  96. edsl/questions/templates/extract/answering_instructions.jinja +7 -0
  97. edsl/questions/templates/extract/question_presentation.jinja +1 -0
  98. edsl/questions/templates/free_text/__init__.py +0 -0
  99. edsl/questions/templates/free_text/answering_instructions.jinja +0 -0
  100. edsl/questions/templates/free_text/question_presentation.jinja +1 -0
  101. edsl/questions/templates/likert_five/__init__.py +0 -0
  102. edsl/questions/templates/likert_five/answering_instructions.jinja +10 -0
  103. edsl/questions/templates/likert_five/question_presentation.jinja +12 -0
  104. edsl/questions/templates/linear_scale/__init__.py +0 -0
  105. edsl/questions/templates/linear_scale/answering_instructions.jinja +5 -0
  106. edsl/questions/templates/linear_scale/question_presentation.jinja +5 -0
  107. edsl/questions/templates/list/__init__.py +0 -0
  108. edsl/questions/templates/list/answering_instructions.jinja +4 -0
  109. edsl/questions/templates/list/question_presentation.jinja +5 -0
  110. edsl/questions/templates/multiple_choice/__init__.py +0 -0
  111. edsl/questions/templates/multiple_choice/answering_instructions.jinja +9 -0
  112. edsl/questions/templates/multiple_choice/html.jinja +0 -0
  113. edsl/questions/templates/multiple_choice/question_presentation.jinja +12 -0
  114. edsl/questions/templates/numerical/__init__.py +0 -0
  115. edsl/questions/templates/numerical/answering_instructions.jinja +8 -0
  116. edsl/questions/templates/numerical/question_presentation.jinja +7 -0
  117. edsl/questions/templates/rank/answering_instructions.jinja +11 -0
  118. edsl/questions/templates/rank/question_presentation.jinja +15 -0
  119. edsl/questions/templates/top_k/__init__.py +0 -0
  120. edsl/questions/templates/top_k/answering_instructions.jinja +8 -0
  121. edsl/questions/templates/top_k/question_presentation.jinja +22 -0
  122. edsl/questions/templates/yes_no/__init__.py +0 -0
  123. edsl/questions/templates/yes_no/answering_instructions.jinja +6 -0
  124. edsl/questions/templates/yes_no/question_presentation.jinja +12 -0
  125. edsl/results/Dataset.py +20 -0
  126. edsl/results/DatasetExportMixin.py +41 -47
  127. edsl/results/DatasetTree.py +145 -0
  128. edsl/results/Result.py +32 -5
  129. edsl/results/Results.py +131 -45
  130. edsl/results/ResultsDBMixin.py +3 -3
  131. edsl/results/Selector.py +118 -0
  132. edsl/results/tree_explore.py +115 -0
  133. edsl/scenarios/Scenario.py +10 -4
  134. edsl/scenarios/ScenarioList.py +348 -39
  135. edsl/scenarios/ScenarioListExportMixin.py +9 -0
  136. edsl/study/SnapShot.py +8 -1
  137. edsl/surveys/RuleCollection.py +2 -2
  138. edsl/surveys/Survey.py +634 -315
  139. edsl/surveys/SurveyExportMixin.py +71 -9
  140. edsl/surveys/SurveyFlowVisualizationMixin.py +2 -1
  141. edsl/surveys/SurveyQualtricsImport.py +75 -4
  142. edsl/surveys/instructions/ChangeInstruction.py +47 -0
  143. edsl/surveys/instructions/Instruction.py +34 -0
  144. edsl/surveys/instructions/InstructionCollection.py +77 -0
  145. edsl/surveys/instructions/__init__.py +0 -0
  146. edsl/templates/error_reporting/base.html +24 -0
  147. edsl/templates/error_reporting/exceptions_by_model.html +35 -0
  148. edsl/templates/error_reporting/exceptions_by_question_name.html +17 -0
  149. edsl/templates/error_reporting/exceptions_by_type.html +17 -0
  150. edsl/templates/error_reporting/interview_details.html +111 -0
  151. edsl/templates/error_reporting/interviews.html +10 -0
  152. edsl/templates/error_reporting/overview.html +5 -0
  153. edsl/templates/error_reporting/performance_plot.html +2 -0
  154. edsl/templates/error_reporting/report.css +74 -0
  155. edsl/templates/error_reporting/report.html +118 -0
  156. edsl/templates/error_reporting/report.js +25 -0
  157. {edsl-0.1.33.dev1.dist-info → edsl-0.1.33.dev2.dist-info}/METADATA +4 -2
  158. edsl-0.1.33.dev2.dist-info/RECORD +289 -0
  159. edsl/jobs/interviews/InterviewTaskBuildingMixin.py +0 -286
  160. edsl/utilities/gcp_bucket/simple_example.py +0 -9
  161. edsl-0.1.33.dev1.dist-info/RECORD +0 -209
  162. {edsl-0.1.33.dev1.dist-info → edsl-0.1.33.dev2.dist-info}/LICENSE +0 -0
  163. {edsl-0.1.33.dev1.dist-info → edsl-0.1.33.dev2.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,133 @@
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
6
3
  from jinja2 import Template
7
4
 
5
+ from pydantic import BaseModel, Field
6
+ from typing import Optional, Literal
7
+
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
+
12
+ from edsl.questions.ResponseValidatorABC import ResponseValidatorABC
13
+ from edsl.questions.ResponseValidatorABC import BaseResponse
14
+
15
+ from edsl.exceptions import QuestionAnswerValidationError
16
+
17
+ from pydantic import BaseModel, Field, create_model
18
+
19
+ from typing import List, Any, Literal
20
+
21
+
22
+ def create_response_model(choices: List[str], permissive: bool = False):
23
+ """
24
+ Create a ChoiceResponse model class with a predefined list of choices.
25
+
26
+ :param choices: A list of allowed values for the answer field.
27
+ :param permissive: If True, any value will be accepted as an answer.
28
+ :return: A new Pydantic model class.
29
+ """
30
+ # Convert the choices list to a tuple for use with Literal
31
+ choice_tuple = tuple(choices)
32
+
33
+ if not permissive:
34
+
35
+ class ChoiceResponse(BaseModel):
36
+ answer: Literal[choice_tuple] = Field(description="Selected choice")
37
+ comment: Optional[str] = Field(None, description="Optional comment field")
38
+ generated_tokens: Optional[Any] = Field(
39
+ None, description="Generated tokens"
40
+ )
41
+
42
+ class Config:
43
+ @staticmethod
44
+ def json_schema_extra(schema: dict, model: BaseModel) -> None:
45
+ for prop in schema.get("properties", {}).values():
46
+ if prop.get("title") == "answer":
47
+ prop["enum"] = choices
48
+
49
+ else:
50
+
51
+ class ChoiceResponse(BaseModel):
52
+ answer: Any = Field(description="Selected choice (can be any value)")
53
+ comment: Optional[str] = Field(None, description="Optional comment field")
54
+ generated_tokens: Optional[Any] = Field(
55
+ None, description="Generated tokens"
56
+ )
57
+
58
+ class Config:
59
+ @staticmethod
60
+ def json_schema_extra(schema: dict, model: BaseModel) -> None:
61
+ for prop in schema.get("properties", {}).values():
62
+ if prop.get("title") == "answer":
63
+ prop["description"] += f". Suggested choices are: {choices}"
64
+ schema["title"] += " (Permissive)"
65
+
66
+ return ChoiceResponse
67
+
68
+
69
+ def fix_multiple_choice(response, question_options, use_code, verbose=False):
70
+ """Fix the response to a multiple choice question.
71
+ Respnse is a dictionary with keys:
72
+ - answer: the answer code
73
+ - generated_tokens: the generated tokens
74
+ - comment: the comment
75
+ """
76
+ pass
77
+
78
+
79
+ class MultipleChoiceResponseValidator(ResponseValidatorABC):
80
+ required_params = ["question_options", "use_code"]
81
+
82
+ def fix(self, response, verbose=False):
83
+ response_text = str(response.get("answer"))
84
+ if response_text is None:
85
+ response_text = response.get("generated_tokens", "")
86
+
87
+ if verbose:
88
+ print(f"Invalid generated tokens was: {response_text}")
89
+
90
+ matches = []
91
+ for idx, option in enumerate(self.question_options):
92
+ if verbose:
93
+ print("The options are: ", self.question_options)
94
+ if str(option) in response_text:
95
+ if verbose:
96
+ print("Match found with option ", option)
97
+ if option not in matches:
98
+ matches.append(option)
99
+
100
+ if verbose:
101
+ print("The matches are: ", matches)
102
+ if len(matches) == 1:
103
+ proposed_data = {
104
+ "answer": matches[0],
105
+ "generated_tokens": response.get("generated_tokens", None),
106
+ }
107
+ try:
108
+ self.response_model(**proposed_data)
109
+ return proposed_data
110
+ except Exception as e:
111
+ if verbose:
112
+ print(f"Proposed solution {proposed_data} is invalid. Error: {e}")
113
+ return response
114
+
115
+ valid_examples = [
116
+ ({"answer": 1}, {"question_options": ["Good", "Great", "OK", "Bad"]})
117
+ ]
118
+
119
+ invalid_examples = [
120
+ (
121
+ {"answer": -1},
122
+ {"question_options": ["Good", "Great", "OK", "Bad"]},
123
+ "Answer code must be a non-negative integer",
124
+ ),
125
+ (
126
+ {"answer": None},
127
+ {"question_options": ["Good", "Great", "OK", "Bad"]},
128
+ "Answer code must not be missing.",
129
+ ),
130
+ ]
10
131
 
11
132
 
12
133
  class QuestionMultipleChoice(QuestionBase):
@@ -21,49 +142,48 @@ class QuestionMultipleChoice(QuestionBase):
21
142
  question_options: Union[
22
143
  list[str], list[list], list[float], list[int]
23
144
  ] = QuestionOptionsDescriptor()
145
+ _response_model = None
146
+ response_validator_class = MultipleChoiceResponseValidator
24
147
 
25
148
  def __init__(
26
149
  self,
27
150
  question_name: str,
28
151
  question_text: str,
29
152
  question_options: Union[list[str], list[list], list[float], list[int]],
153
+ include_comment: bool = True,
154
+ use_code: bool = False,
155
+ answering_instructions: Optional[str] = None,
156
+ question_presentation: Optional[str] = None,
157
+ permissive: bool = False,
30
158
  ):
31
159
  """Instantiate a new QuestionMultipleChoice.
32
160
 
33
161
  :param question_name: The name of the question.
34
162
  :param question_text: The text of the question.
35
163
  :param question_options: The options the agent should select from.
164
+
36
165
  """
37
166
  self.question_name = question_name
38
167
  self.question_text = question_text
39
168
  self.question_options = question_options
40
169
 
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
170
+ self._include_comment = include_comment
171
+ self.use_code = use_code
172
+ self.answering_instructions = answering_instructions
173
+ self.question_presentation = question_presentation
174
+ self.permissive = permissive
45
175
 
46
176
  ################
47
177
  # Answer methods
48
178
  ################
49
- def _validate_answer(
50
- self, answer: dict[str, Union[str, int]]
51
- ) -> dict[str, Union[str, int]]:
52
- """Validate the answer.
53
179
 
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
180
+ def create_response_model(self):
181
+ if self.use_code:
182
+ return create_response_model(
183
+ list(range(len(self.question_options))), self.permissive
184
+ )
185
+ else:
186
+ return create_response_model(self.question_options, self.permissive)
67
187
 
68
188
  def _translate_answer_code_to_answer(
69
189
  self, answer_code: int, scenario: Optional["Scenario"] = None
@@ -74,11 +194,11 @@ class QuestionMultipleChoice(QuestionBase):
74
194
  The question options might be templates, so they need to be rendered with the scenario.
75
195
 
76
196
  >>> q = QuestionMultipleChoice.example()
77
- >>> q._translate_answer_code_to_answer(0, {})
197
+ >>> q._translate_answer_code_to_answer('Good', {})
78
198
  'Good'
79
199
 
80
200
  >>> 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"]})
201
+ >>> q._translate_answer_code_to_answer('Happy', {"emotion": ["Happy", "Sad"]})
82
202
  'Happy'
83
203
 
84
204
  """
@@ -95,31 +215,17 @@ class QuestionMultipleChoice(QuestionBase):
95
215
  question_option_key = list(meta.find_undeclared_variables(parsed_content))[
96
216
  0
97
217
  ]
98
- # breakpoint()
99
218
  translated_options = scenario.get(question_option_key)
100
219
  else:
101
220
  translated_options = [
102
221
  Template(str(option)).render(scenario)
103
222
  for option in self.question_options
104
223
  ]
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)
224
+ if self._use_code:
225
+ return translated_options[int(answer_code)]
117
226
  else:
118
- answer = random.choice(range(len(self.question_options)))
119
- return {
120
- "answer": answer,
121
- "comment": random_string(),
122
- }
227
+ # return translated_options[answer_code]
228
+ return answer_code
123
229
 
124
230
  @property
125
231
  def question_html_content(self) -> str:
@@ -153,33 +259,36 @@ class QuestionMultipleChoice(QuestionBase):
153
259
  # Example
154
260
  ################
155
261
  @classmethod
156
- def example(cls) -> QuestionMultipleChoice:
262
+ @inject_exception
263
+ def example(cls, include_comment=False, use_code=False) -> QuestionMultipleChoice:
157
264
  """Return an example instance."""
158
265
  return cls(
159
266
  question_text="How are you?",
160
267
  question_options=["Good", "Great", "OK", "Bad"],
161
268
  question_name="how_feeling",
269
+ include_comment=include_comment,
270
+ use_code=use_code,
162
271
  )
163
272
 
164
273
 
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
274
+ # def main():
275
+ # """Create an example QuestionMultipleChoice and test its methods."""
276
+ # from edsl.questions.QuestionMultipleChoice import QuestionMultipleChoice
277
+
278
+ # q = QuestionMultipleChoice.example()
279
+ # q.question_text
280
+ # q.question_options
281
+ # q.question_name
282
+ # # validate an answer
283
+ # q._validate_answer({"answer": 0, "comment": "I like custard"})
284
+ # # translate answer code
285
+ # q._translate_answer_code_to_answer(0, {})
286
+ # # simulate answer
287
+ # q._simulate_answer()
288
+ # q._simulate_answer(human_readable=False)
289
+ # # serialization (inherits from Question)
290
+ # q.to_dict()
291
+ # assert q.from_dict(q.to_dict()) == q
183
292
 
184
293
 
185
294
  if __name__ == "__main__":