edsl 0.1.33__py3-none-any.whl → 0.1.33.dev1__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 (180) hide show
  1. edsl/Base.py +3 -9
  2. edsl/__init__.py +3 -8
  3. edsl/__version__.py +1 -1
  4. edsl/agents/Agent.py +8 -40
  5. edsl/agents/AgentList.py +0 -43
  6. edsl/agents/Invigilator.py +219 -135
  7. edsl/agents/InvigilatorBase.py +59 -148
  8. edsl/agents/{PromptConstructor.py → PromptConstructionMixin.py} +89 -138
  9. edsl/agents/__init__.py +0 -1
  10. edsl/config.py +56 -47
  11. edsl/coop/coop.py +7 -50
  12. edsl/data/Cache.py +1 -35
  13. edsl/data_transfer_models.py +38 -73
  14. edsl/enums.py +0 -4
  15. edsl/exceptions/language_models.py +1 -25
  16. edsl/exceptions/questions.py +5 -62
  17. edsl/exceptions/results.py +0 -4
  18. edsl/inference_services/AnthropicService.py +11 -13
  19. edsl/inference_services/AwsBedrock.py +17 -19
  20. edsl/inference_services/AzureAI.py +20 -37
  21. edsl/inference_services/GoogleService.py +12 -16
  22. edsl/inference_services/GroqService.py +0 -2
  23. edsl/inference_services/InferenceServiceABC.py +3 -58
  24. edsl/inference_services/OpenAIService.py +54 -48
  25. edsl/inference_services/models_available_cache.py +6 -0
  26. edsl/inference_services/registry.py +0 -6
  27. edsl/jobs/Answers.py +12 -10
  28. edsl/jobs/Jobs.py +21 -36
  29. edsl/jobs/buckets/BucketCollection.py +15 -24
  30. edsl/jobs/buckets/TokenBucket.py +14 -93
  31. edsl/jobs/interviews/Interview.py +78 -366
  32. edsl/jobs/interviews/InterviewExceptionEntry.py +19 -85
  33. edsl/jobs/interviews/InterviewTaskBuildingMixin.py +286 -0
  34. edsl/jobs/interviews/{InterviewExceptionCollection.py → interview_exception_tracking.py} +68 -14
  35. edsl/jobs/interviews/retry_management.py +37 -0
  36. edsl/jobs/runners/JobsRunnerAsyncio.py +175 -146
  37. edsl/jobs/runners/JobsRunnerStatusMixin.py +333 -0
  38. edsl/jobs/tasks/QuestionTaskCreator.py +23 -30
  39. edsl/jobs/tasks/TaskHistory.py +213 -148
  40. edsl/language_models/LanguageModel.py +156 -261
  41. edsl/language_models/ModelList.py +2 -2
  42. edsl/language_models/RegisterLanguageModelsMeta.py +29 -14
  43. edsl/language_models/registry.py +6 -23
  44. edsl/language_models/repair.py +19 -0
  45. edsl/prompts/Prompt.py +2 -52
  46. edsl/questions/AnswerValidatorMixin.py +26 -23
  47. edsl/questions/QuestionBase.py +249 -329
  48. edsl/questions/QuestionBudget.py +41 -99
  49. edsl/questions/QuestionCheckBox.py +35 -227
  50. edsl/questions/QuestionExtract.py +27 -98
  51. edsl/questions/QuestionFreeText.py +29 -52
  52. edsl/questions/QuestionFunctional.py +0 -7
  53. edsl/questions/QuestionList.py +22 -141
  54. edsl/questions/QuestionMultipleChoice.py +65 -159
  55. edsl/questions/QuestionNumerical.py +46 -88
  56. edsl/questions/QuestionRank.py +24 -182
  57. edsl/questions/RegisterQuestionsMeta.py +12 -31
  58. edsl/questions/__init__.py +4 -3
  59. edsl/questions/derived/QuestionLikertFive.py +5 -10
  60. edsl/questions/derived/QuestionLinearScale.py +2 -15
  61. edsl/questions/derived/QuestionTopK.py +1 -10
  62. edsl/questions/derived/QuestionYesNo.py +3 -24
  63. edsl/questions/descriptors.py +7 -43
  64. edsl/questions/question_registry.py +2 -6
  65. edsl/results/Dataset.py +0 -20
  66. edsl/results/DatasetExportMixin.py +48 -46
  67. edsl/results/Result.py +5 -32
  68. edsl/results/Results.py +46 -135
  69. edsl/results/ResultsDBMixin.py +3 -3
  70. edsl/scenarios/FileStore.py +10 -71
  71. edsl/scenarios/Scenario.py +25 -96
  72. edsl/scenarios/ScenarioImageMixin.py +2 -2
  73. edsl/scenarios/ScenarioList.py +39 -361
  74. edsl/scenarios/ScenarioListExportMixin.py +0 -9
  75. edsl/scenarios/ScenarioListPdfMixin.py +4 -150
  76. edsl/study/SnapShot.py +1 -8
  77. edsl/study/Study.py +0 -32
  78. edsl/surveys/Rule.py +1 -10
  79. edsl/surveys/RuleCollection.py +5 -21
  80. edsl/surveys/Survey.py +310 -636
  81. edsl/surveys/SurveyExportMixin.py +9 -71
  82. edsl/surveys/SurveyFlowVisualizationMixin.py +1 -2
  83. edsl/surveys/SurveyQualtricsImport.py +4 -75
  84. edsl/utilities/gcp_bucket/simple_example.py +9 -0
  85. edsl/utilities/utilities.py +1 -9
  86. {edsl-0.1.33.dist-info → edsl-0.1.33.dev1.dist-info}/METADATA +2 -5
  87. edsl-0.1.33.dev1.dist-info/RECORD +209 -0
  88. edsl/TemplateLoader.py +0 -24
  89. edsl/auto/AutoStudy.py +0 -117
  90. edsl/auto/StageBase.py +0 -230
  91. edsl/auto/StageGenerateSurvey.py +0 -178
  92. edsl/auto/StageLabelQuestions.py +0 -125
  93. edsl/auto/StagePersona.py +0 -61
  94. edsl/auto/StagePersonaDimensionValueRanges.py +0 -88
  95. edsl/auto/StagePersonaDimensionValues.py +0 -74
  96. edsl/auto/StagePersonaDimensions.py +0 -69
  97. edsl/auto/StageQuestions.py +0 -73
  98. edsl/auto/SurveyCreatorPipeline.py +0 -21
  99. edsl/auto/utilities.py +0 -224
  100. edsl/coop/PriceFetcher.py +0 -58
  101. edsl/inference_services/MistralAIService.py +0 -120
  102. edsl/inference_services/TestService.py +0 -80
  103. edsl/inference_services/TogetherAIService.py +0 -170
  104. edsl/jobs/FailedQuestion.py +0 -78
  105. edsl/jobs/runners/JobsRunnerStatus.py +0 -331
  106. edsl/language_models/fake_openai_call.py +0 -15
  107. edsl/language_models/fake_openai_service.py +0 -61
  108. edsl/language_models/utilities.py +0 -61
  109. edsl/questions/QuestionBaseGenMixin.py +0 -133
  110. edsl/questions/QuestionBasePromptsMixin.py +0 -266
  111. edsl/questions/Quick.py +0 -41
  112. edsl/questions/ResponseValidatorABC.py +0 -170
  113. edsl/questions/decorators.py +0 -21
  114. edsl/questions/prompt_templates/question_budget.jinja +0 -13
  115. edsl/questions/prompt_templates/question_checkbox.jinja +0 -32
  116. edsl/questions/prompt_templates/question_extract.jinja +0 -11
  117. edsl/questions/prompt_templates/question_free_text.jinja +0 -3
  118. edsl/questions/prompt_templates/question_linear_scale.jinja +0 -11
  119. edsl/questions/prompt_templates/question_list.jinja +0 -17
  120. edsl/questions/prompt_templates/question_multiple_choice.jinja +0 -33
  121. edsl/questions/prompt_templates/question_numerical.jinja +0 -37
  122. edsl/questions/templates/__init__.py +0 -0
  123. edsl/questions/templates/budget/__init__.py +0 -0
  124. edsl/questions/templates/budget/answering_instructions.jinja +0 -7
  125. edsl/questions/templates/budget/question_presentation.jinja +0 -7
  126. edsl/questions/templates/checkbox/__init__.py +0 -0
  127. edsl/questions/templates/checkbox/answering_instructions.jinja +0 -10
  128. edsl/questions/templates/checkbox/question_presentation.jinja +0 -22
  129. edsl/questions/templates/extract/__init__.py +0 -0
  130. edsl/questions/templates/extract/answering_instructions.jinja +0 -7
  131. edsl/questions/templates/extract/question_presentation.jinja +0 -1
  132. edsl/questions/templates/free_text/__init__.py +0 -0
  133. edsl/questions/templates/free_text/answering_instructions.jinja +0 -0
  134. edsl/questions/templates/free_text/question_presentation.jinja +0 -1
  135. edsl/questions/templates/likert_five/__init__.py +0 -0
  136. edsl/questions/templates/likert_five/answering_instructions.jinja +0 -10
  137. edsl/questions/templates/likert_five/question_presentation.jinja +0 -12
  138. edsl/questions/templates/linear_scale/__init__.py +0 -0
  139. edsl/questions/templates/linear_scale/answering_instructions.jinja +0 -5
  140. edsl/questions/templates/linear_scale/question_presentation.jinja +0 -5
  141. edsl/questions/templates/list/__init__.py +0 -0
  142. edsl/questions/templates/list/answering_instructions.jinja +0 -4
  143. edsl/questions/templates/list/question_presentation.jinja +0 -5
  144. edsl/questions/templates/multiple_choice/__init__.py +0 -0
  145. edsl/questions/templates/multiple_choice/answering_instructions.jinja +0 -9
  146. edsl/questions/templates/multiple_choice/html.jinja +0 -0
  147. edsl/questions/templates/multiple_choice/question_presentation.jinja +0 -12
  148. edsl/questions/templates/numerical/__init__.py +0 -0
  149. edsl/questions/templates/numerical/answering_instructions.jinja +0 -8
  150. edsl/questions/templates/numerical/question_presentation.jinja +0 -7
  151. edsl/questions/templates/rank/__init__.py +0 -0
  152. edsl/questions/templates/rank/answering_instructions.jinja +0 -11
  153. edsl/questions/templates/rank/question_presentation.jinja +0 -15
  154. edsl/questions/templates/top_k/__init__.py +0 -0
  155. edsl/questions/templates/top_k/answering_instructions.jinja +0 -8
  156. edsl/questions/templates/top_k/question_presentation.jinja +0 -22
  157. edsl/questions/templates/yes_no/__init__.py +0 -0
  158. edsl/questions/templates/yes_no/answering_instructions.jinja +0 -6
  159. edsl/questions/templates/yes_no/question_presentation.jinja +0 -12
  160. edsl/results/DatasetTree.py +0 -145
  161. edsl/results/Selector.py +0 -118
  162. edsl/results/tree_explore.py +0 -115
  163. edsl/surveys/instructions/ChangeInstruction.py +0 -47
  164. edsl/surveys/instructions/Instruction.py +0 -34
  165. edsl/surveys/instructions/InstructionCollection.py +0 -77
  166. edsl/surveys/instructions/__init__.py +0 -0
  167. edsl/templates/error_reporting/base.html +0 -24
  168. edsl/templates/error_reporting/exceptions_by_model.html +0 -35
  169. edsl/templates/error_reporting/exceptions_by_question_name.html +0 -17
  170. edsl/templates/error_reporting/exceptions_by_type.html +0 -17
  171. edsl/templates/error_reporting/interview_details.html +0 -116
  172. edsl/templates/error_reporting/interviews.html +0 -10
  173. edsl/templates/error_reporting/overview.html +0 -5
  174. edsl/templates/error_reporting/performance_plot.html +0 -2
  175. edsl/templates/error_reporting/report.css +0 -74
  176. edsl/templates/error_reporting/report.html +0 -118
  177. edsl/templates/error_reporting/report.js +0 -25
  178. edsl-0.1.33.dist-info/RECORD +0 -295
  179. {edsl-0.1.33.dist-info → edsl-0.1.33.dev1.dist-info}/LICENSE +0 -0
  180. {edsl-0.1.33.dist-info → edsl-0.1.33.dev1.dist-info}/WHEEL +0 -0
@@ -1,62 +1,23 @@
1
1
  from __future__ import annotations
2
+ import textwrap
2
3
  from typing import Any, Optional
3
4
  from uuid import uuid4
4
-
5
- from pydantic import field_validator
6
-
7
5
  from edsl.questions.QuestionBase import QuestionBase
8
- from edsl.questions.ResponseValidatorABC import ResponseValidatorABC
9
-
10
- from edsl.exceptions import QuestionAnswerValidationError
11
- from edsl.questions.decorators import inject_exception
12
-
13
- from pydantic import BaseModel
14
- from typing import Optional, Any, List
15
-
16
- from edsl.exceptions import QuestionAnswerValidationError
17
-
18
-
19
- class FreeTextResponse(BaseModel):
20
- """
21
- Validator for free text response questions.
22
- """
23
-
24
- answer: str
25
- generated_tokens: Optional[str] = None
26
-
27
-
28
- class FreeTextResponseValidator(ResponseValidatorABC):
29
- required_params = []
30
- valid_examples = [({"answer": "This is great"}, {})]
31
- invalid_examples = [
32
- (
33
- {"answer": None},
34
- {},
35
- "Answer code must not be missing.",
36
- ),
37
- ]
38
-
39
- def fix(self, response, verbose=False):
40
- return {
41
- "answer": str(response.get("generated_tokens")),
42
- "generated_tokens": str(response.get("generated_tokens")),
43
- }
44
6
 
45
7
 
46
8
  class QuestionFreeText(QuestionBase):
47
9
  """This question prompts the agent to respond with free text."""
48
10
 
49
11
  question_type = "free_text"
50
- _response_model = FreeTextResponse
51
- response_validator_class = FreeTextResponseValidator
52
-
53
- def __init__(
54
- self,
55
- question_name: str,
56
- question_text: str,
57
- answering_instructions: Optional[str] = None,
58
- question_presentation: Optional[str] = None,
59
- ):
12
+ default_instructions = textwrap.dedent(
13
+ """\
14
+ You are being asked the following question: {{question_text}}
15
+ Return a valid JSON formatted like this:
16
+ {"answer": "<put free text answer here>"}
17
+ """
18
+ )
19
+
20
+ def __init__(self, question_name: str, question_text: str):
60
21
  """Instantiate a new QuestionFreeText.
61
22
 
62
23
  :param question_name: The name of the question.
@@ -64,8 +25,25 @@ class QuestionFreeText(QuestionBase):
64
25
  """
65
26
  self.question_name = question_name
66
27
  self.question_text = question_text
67
- self.answering_instructions = answering_instructions
68
- self.question_presentation = question_presentation
28
+
29
+ ################
30
+ # Answer methods
31
+ ################
32
+ def _validate_answer(self, answer: Any) -> dict[str, str]:
33
+ """Validate the answer."""
34
+ self._validate_answer_template_basic(answer)
35
+ self._validate_answer_key_value(answer, "answer", str)
36
+ return answer
37
+
38
+ def _translate_answer_code_to_answer(self, answer, scenario: "Scenario" = None):
39
+ """Do nothing, because the answer is already in a human-readable format."""
40
+ return answer
41
+
42
+ def _simulate_answer(self, human_readable: bool = True) -> dict[str, str]:
43
+ """Simulate a valid answer for debugging purposes."""
44
+ from edsl.utilities.utilities import random_string
45
+
46
+ return {"answer": random_string()}
69
47
 
70
48
  @property
71
49
  def question_html_content(self) -> str:
@@ -81,7 +59,6 @@ class QuestionFreeText(QuestionBase):
81
59
  return question_html_content
82
60
 
83
61
  @classmethod
84
- @inject_exception
85
62
  def example(cls, randomize: bool = False) -> QuestionFreeText:
86
63
  """Return an example instance of a free text question."""
87
64
  addition = "" if not randomize else str(uuid4())
@@ -39,9 +39,6 @@ class QuestionFunctional(QuestionBase):
39
39
  function_source_code = ""
40
40
  function_name = ""
41
41
 
42
- _response_model = None
43
- response_validator_class = None
44
-
45
42
  def __init__(
46
43
  self,
47
44
  question_name: str,
@@ -100,10 +97,6 @@ class QuestionFunctional(QuestionBase):
100
97
  """Required by Question, but not used by QuestionFunctional."""
101
98
  raise NotImplementedError
102
99
 
103
- @property
104
- def question_html_content(self) -> str:
105
- return "NA for QuestionFunctional"
106
-
107
100
  @add_edsl_version
108
101
  def to_dict(self):
109
102
  return {
@@ -4,123 +4,6 @@ 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
124
7
 
125
8
 
126
9
  class QuestionList(QuestionBase):
@@ -128,38 +11,43 @@ class QuestionList(QuestionBase):
128
11
 
129
12
  question_type = "list"
130
13
  max_list_items: int = IntegerOrNoneDescriptor()
131
- _response_model = None
132
- response_validator_class = ListResponseValidator
133
14
 
134
15
  def __init__(
135
16
  self,
136
17
  question_name: str,
137
18
  question_text: str,
138
19
  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,
143
20
  ):
144
21
  """Instantiate a new QuestionList.
145
22
 
146
23
  :param question_name: The name of the question.
147
24
  :param question_text: The text of the question.
148
25
  :param max_list_items: The maximum number of items that can be in the answer list.
149
-
150
- >>> QuestionList.example().self_check()
151
26
  """
152
27
  self.question_name = question_name
153
28
  self.question_text = question_text
154
29
  self.max_list_items = max_list_items
155
- self.permissive = permissive
156
30
 
157
- self.include_comment = include_comment
158
- self.answering_instructions = answering_instructions
159
- self.question_presentations = question_presentation
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
160
49
 
161
- def create_response_model(self):
162
- return create_model(self.max_list_items, self.permissive)
50
+ return {"answer": [random_string() for _ in range(num_items)]}
163
51
 
164
52
  @property
165
53
  def question_html_content(self) -> str:
@@ -190,17 +78,12 @@ class QuestionList(QuestionBase):
190
78
  # Helpful methods
191
79
  ################
192
80
  @classmethod
193
- @inject_exception
194
- def example(
195
- cls, include_comment=True, max_list_items=None, permissive=False
196
- ) -> QuestionList:
81
+ def example(cls) -> QuestionList:
197
82
  """Return an example of a list question."""
198
83
  return cls(
199
84
  question_name="list_of_foods",
200
85
  question_text="What are your favorite foods?",
201
- include_comment=include_comment,
202
- max_list_items=max_list_items,
203
- permissive=permissive,
86
+ max_list_items=5,
204
87
  )
205
88
 
206
89
 
@@ -208,7 +91,7 @@ def main():
208
91
  """Create an example of a list question and demonstrate its functionality."""
209
92
  from edsl.questions.QuestionList import QuestionList
210
93
 
211
- q = QuestionList.example(max_list_items=5)
94
+ q = QuestionList.example()
212
95
  q.question_text
213
96
  q.question_name
214
97
  q.max_list_items
@@ -224,8 +107,6 @@ def main():
224
107
  q.to_dict()
225
108
  assert q.from_dict(q.to_dict()) == q
226
109
 
227
-
228
- if __name__ == "__main__":
229
110
  import doctest
230
111
 
231
112
  doctest.testmod(optionflags=doctest.ELLIPSIS)
@@ -1,114 +1,12 @@
1
1
  from __future__ import annotations
2
- from typing import Union, Literal, Optional, List, Any
3
-
2
+ import time
3
+ from typing import Union
4
+ import random
5
+ from typing import Optional
4
6
  from jinja2 import Template
5
- from pydantic import BaseModel, Field
6
7
 
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
- ]
112
10
 
113
11
 
114
12
  class QuestionMultipleChoice(QuestionBase):
@@ -123,53 +21,49 @@ class QuestionMultipleChoice(QuestionBase):
123
21
  question_options: Union[
124
22
  list[str], list[list], list[float], list[int]
125
23
  ] = QuestionOptionsDescriptor()
126
- _response_model = None
127
- response_validator_class = MultipleChoiceResponseValidator
128
24
 
129
25
  def __init__(
130
26
  self,
131
27
  question_name: str,
132
28
  question_text: str,
133
29
  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,
139
30
  ):
140
31
  """Instantiate a new QuestionMultipleChoice.
141
32
 
142
33
  :param question_name: The name of the question.
143
34
  :param question_text: The text of the question.
144
35
  :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
-
151
36
  """
152
37
  self.question_name = question_name
153
38
  self.question_text = question_text
154
39
  self.question_options = question_options
155
40
 
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
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
161
45
 
162
46
  ################
163
47
  # Answer methods
164
48
  ################
49
+ def _validate_answer(
50
+ self, answer: dict[str, Union[str, int]]
51
+ ) -> dict[str, Union[str, int]]:
52
+ """Validate the answer.
165
53
 
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)
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
173
67
 
174
68
  def _translate_answer_code_to_answer(
175
69
  self, answer_code: int, scenario: Optional["Scenario"] = None
@@ -180,14 +74,15 @@ class QuestionMultipleChoice(QuestionBase):
180
74
  The question options might be templates, so they need to be rendered with the scenario.
181
75
 
182
76
  >>> q = QuestionMultipleChoice.example()
183
- >>> q._translate_answer_code_to_answer('Good', {})
77
+ >>> q._translate_answer_code_to_answer(0, {})
184
78
  'Good'
185
79
 
186
80
  >>> q = QuestionMultipleChoice(question_name="how_feeling", question_text="How are you?", question_options=["{{emotion[0]}}", "emotion[1]"])
187
- >>> q._translate_answer_code_to_answer('Happy', {"emotion": ["Happy", "Sad"]})
81
+ >>> q._translate_answer_code_to_answer(0, {"emotion": ["Happy", "Sad"]})
188
82
  'Happy'
189
83
 
190
84
  """
85
+ from edsl.scenarios.Scenario import Scenario
191
86
 
192
87
  scenario = scenario or Scenario()
193
88
 
@@ -200,17 +95,31 @@ class QuestionMultipleChoice(QuestionBase):
200
95
  question_option_key = list(meta.find_undeclared_variables(parsed_content))[
201
96
  0
202
97
  ]
98
+ # breakpoint()
203
99
  translated_options = scenario.get(question_option_key)
204
100
  else:
205
101
  translated_options = [
206
102
  Template(str(option)).render(scenario)
207
103
  for option in self.question_options
208
104
  ]
209
- if self._use_code:
210
- return translated_options[int(answer_code)]
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)
211
117
  else:
212
- # return translated_options[answer_code]
213
- return answer_code
118
+ answer = random.choice(range(len(self.question_options)))
119
+ return {
120
+ "answer": answer,
121
+ "comment": random_string(),
122
+ }
214
123
 
215
124
  @property
216
125
  def question_html_content(self) -> str:
@@ -244,36 +153,33 @@ class QuestionMultipleChoice(QuestionBase):
244
153
  # Example
245
154
  ################
246
155
  @classmethod
247
- @inject_exception
248
- def example(cls, include_comment=False, use_code=False) -> QuestionMultipleChoice:
156
+ def example(cls) -> QuestionMultipleChoice:
249
157
  """Return an example instance."""
250
158
  return cls(
251
159
  question_text="How are you?",
252
160
  question_options=["Good", "Great", "OK", "Bad"],
253
161
  question_name="how_feeling",
254
- include_comment=include_comment,
255
- use_code=use_code,
256
162
  )
257
163
 
258
164
 
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
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
277
183
 
278
184
 
279
185
  if __name__ == "__main__":