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
@@ -0,0 +1,170 @@
1
+ from abc import ABC, abstractmethod
2
+ from pydantic import BaseModel, Field, field_validator
3
+
4
+ # from decimal import Decimal
5
+ from typing import Optional, Any, List, TypedDict
6
+
7
+ from edsl.exceptions import QuestionAnswerValidationError
8
+ from pydantic import ValidationError
9
+
10
+
11
+ class BaseResponse(BaseModel):
12
+ answer: Any
13
+ comment: Optional[str] = None
14
+ generated_tokens: Optional[str] = None
15
+
16
+
17
+ class ResponseValidatorABC(ABC):
18
+ required_params: List[str] = []
19
+
20
+ def __init_subclass__(cls, **kwargs):
21
+ super().__init_subclass__(**kwargs)
22
+ required_class_vars = ["required_params", "valid_examples", "invalid_examples"]
23
+ for var in required_class_vars:
24
+ if not hasattr(cls, var):
25
+ raise ValueError(f"Class {cls.__name__} must have a '{var}' attribute.")
26
+
27
+ def __init__(
28
+ self,
29
+ response_model: type[BaseModel],
30
+ exception_to_throw: Optional[Exception] = None,
31
+ override_answer: Optional[dict] = None,
32
+ **kwargs,
33
+ ):
34
+ self.response_model = response_model
35
+ self.exception_to_throw = exception_to_throw # for testing
36
+ self.override_answer = override_answer # for testing
37
+ self.original_exception = None
38
+
39
+ # Validate required parameters
40
+ missing_params = [
41
+ param for param in self.required_params if param not in kwargs
42
+ ]
43
+ if missing_params:
44
+ raise ValueError(
45
+ f"Missing required parameters: {', '.join(missing_params)}"
46
+ )
47
+
48
+ # Set attributes
49
+ for key, value in kwargs.items():
50
+ setattr(self, key, value)
51
+
52
+ if not hasattr(self, "permissive"):
53
+ self.permissive = False
54
+
55
+ self.fixes_tried = 0
56
+
57
+ class RawEdslAnswerDict(TypedDict):
58
+ answer: Any
59
+ comment: Optional[str]
60
+ generated_tokens: Optional[str]
61
+
62
+ def _preprocess(self, data: RawEdslAnswerDict) -> RawEdslAnswerDict:
63
+ """This is for testing purposes. A question can be given an exception to throw or an answer to always return.
64
+
65
+ >>> rv = ResponseValidatorABC.example()
66
+ >>> rv.override_answer = {"answer": 42}
67
+ >>> rv.validate({"answer": 23})
68
+ {'answer': 42, 'comment': None, 'generated_tokens': None}
69
+ """
70
+ if self.exception_to_throw:
71
+ raise self.exception_to_throw
72
+ return self.override_answer if self.override_answer else data
73
+
74
+ def _base_validate(self, data: RawEdslAnswerDict) -> BaseModel:
75
+ """This is the main validation function. It takes the response_model and checks the data against it, returning the instantiated model.
76
+
77
+ >>> rv = ResponseValidatorABC.example("numerical")
78
+ >>> rv._base_validate({"answer": 42})
79
+ ConstrainedNumericResponse(answer=42, comment=None, generated_tokens=None)
80
+ """
81
+ try:
82
+ return self.response_model(**data)
83
+ except ValidationError as e:
84
+ raise QuestionAnswerValidationError(e, data=data, model=self.response_model)
85
+
86
+ def post_validation_answer_convert(self, data):
87
+ return data
88
+
89
+ class EdslAnswerDict(TypedDict):
90
+ answer: Any
91
+ comment: Optional[str]
92
+ generated_tokens: Optional[str]
93
+
94
+ def validate(
95
+ self, raw_edsl_answer_dict: RawEdslAnswerDict, fix=False, verbose=False
96
+ ) -> EdslAnswerDict:
97
+ """This is the main validation function.
98
+
99
+ >>> rv = ResponseValidatorABC.example("numerical")
100
+ >>> rv.validate({"answer": 42})
101
+ {'answer': 42, 'comment': None, 'generated_tokens': None}
102
+ >>> rv.max_value
103
+ 86.7
104
+ >>> rv.validate({"answer": "120"})
105
+ Traceback (most recent call last):
106
+ ...
107
+ edsl.exceptions.questions.QuestionAnswerValidationError:...
108
+ >>> from edsl import QuestionNumerical
109
+ >>> q = QuestionNumerical.example()
110
+ >>> q.permissive = True
111
+ >>> rv = q.response_validator
112
+ >>> rv.validate({"answer": "120"})
113
+ {'answer': 120, 'comment': None, 'generated_tokens': None}
114
+ >>> rv.validate({"answer": "poo"})
115
+ Traceback (most recent call last):
116
+ ...
117
+ edsl.exceptions.questions.QuestionAnswerValidationError:...
118
+ """
119
+ proposed_edsl_answer_dict = self._preprocess(raw_edsl_answer_dict)
120
+ try:
121
+ pydantic_edsl_answer: BaseModel = self._base_validate(
122
+ proposed_edsl_answer_dict
123
+ )
124
+ edsl_answer_dict = self._extract_answer(pydantic_edsl_answer)
125
+ return self._post_process(edsl_answer_dict)
126
+ except QuestionAnswerValidationError as e:
127
+ if verbose:
128
+ print(f"Failed to validate {raw_edsl_answer_dict}; {str(e)}")
129
+ return self._handle_exception(e, raw_edsl_answer_dict)
130
+
131
+ def _handle_exception(self, e: Exception, raw_edsl_answer_dict) -> EdslAnswerDict:
132
+ if self.fixes_tried == 0:
133
+ self.original_exception = e
134
+
135
+ if self.fixes_tried == 0 and hasattr(self, "fix"):
136
+ self.fixes_tried += 1
137
+ fixed_data = self.fix(raw_edsl_answer_dict)
138
+ try:
139
+ return self.validate(fixed_data, fix=True)
140
+ except Exception as e:
141
+ pass # we don't log failed fixes
142
+
143
+ raise QuestionAnswerValidationError(
144
+ self.original_exception,
145
+ data=raw_edsl_answer_dict,
146
+ model=self.response_model,
147
+ )
148
+
149
+ def _check_constraints(self, pydantic_edsl_answer: BaseModel) -> dict:
150
+ pass
151
+
152
+ def _extract_answer(self, response: BaseModel) -> EdslAnswerDict:
153
+ return response.model_dump()
154
+
155
+ def _post_process(self, edsl_answer_dict: EdslAnswerDict) -> EdslAnswerDict:
156
+ return edsl_answer_dict
157
+
158
+ @classmethod
159
+ def example(cls, question_type="numerical"):
160
+ from edsl import Question
161
+
162
+ q = Question.example(question_type)
163
+ return q.response_validator
164
+
165
+
166
+ # Example usage
167
+ if __name__ == "__main__":
168
+ import doctest
169
+
170
+ doctest.testmod(optionflags=doctest.ELLIPSIS)
@@ -6,22 +6,21 @@ from edsl.questions.RegisterQuestionsMeta import RegisterQuestionsMeta
6
6
  from edsl.questions.QuestionBase import QuestionBase
7
7
 
8
8
  # Core Questions
9
- from edsl.questions.QuestionBudget import QuestionBudget
10
9
  from edsl.questions.QuestionCheckBox import QuestionCheckBox
11
10
  from edsl.questions.QuestionExtract import QuestionExtract
12
11
  from edsl.questions.QuestionFreeText import QuestionFreeText
13
-
14
12
  from edsl.questions.QuestionFunctional import QuestionFunctional
15
13
  from edsl.questions.QuestionList import QuestionList
16
14
  from edsl.questions.QuestionMultipleChoice import QuestionMultipleChoice
17
15
  from edsl.questions.QuestionNumerical import QuestionNumerical
16
+ from edsl.questions.QuestionBudget import QuestionBudget
18
17
  from edsl.questions.QuestionRank import QuestionRank
19
18
 
20
- # # Questions derived from core questions
19
+ # # # Questions derived from core questions
21
20
  from edsl.questions.derived.QuestionLikertFive import QuestionLikertFive
22
21
  from edsl.questions.derived.QuestionLinearScale import QuestionLinearScale
23
- from edsl.questions.derived.QuestionTopK import QuestionTopK
24
22
  from edsl.questions.derived.QuestionYesNo import QuestionYesNo
23
+ from edsl.questions.derived.QuestionTopK import QuestionTopK
25
24
 
26
25
  # # Compose Questions
27
26
  # from edsl.questions.compose_questions import compose_questions
@@ -0,0 +1,21 @@
1
+ from typing import Optional, Callable, TypeVar
2
+
3
+ T = TypeVar("T")
4
+
5
+
6
+ def inject_exception(func: Callable[..., T]) -> Callable[..., T]:
7
+ def wrapper(
8
+ cls,
9
+ exception_to_throw: Optional[Exception] = None,
10
+ override_answer: Optional[dict] = None,
11
+ *args,
12
+ **kwargs
13
+ ) -> T:
14
+ base_instance = func(cls, *args, **kwargs)
15
+ if exception_to_throw:
16
+ base_instance.exception_to_throw = exception_to_throw
17
+ if override_answer:
18
+ base_instance.override_answer = override_answer
19
+ return base_instance
20
+
21
+ return wrapper
@@ -2,6 +2,8 @@ from __future__ import annotations
2
2
  from typing import Optional
3
3
  from edsl.questions.QuestionMultipleChoice import QuestionMultipleChoice
4
4
 
5
+ from edsl.questions.decorators import inject_exception
6
+
5
7
 
6
8
  class QuestionLikertFive(QuestionMultipleChoice):
7
9
  """This question prompts the agent to respond to a statement on a 5-point Likert scale."""
@@ -14,31 +16,34 @@ class QuestionLikertFive(QuestionMultipleChoice):
14
16
  "Agree",
15
17
  "Strongly agree",
16
18
  ]
17
- # default_instructions = QuestionMultipleChoice.default_instructions
18
19
 
19
20
  def __init__(
20
21
  self,
21
22
  question_name: str,
22
23
  question_text: str,
23
24
  question_options: Optional[list[str]] = likert_options,
25
+ answering_instructions: Optional[str] = None,
26
+ question_presentation: Optional[str] = None,
27
+ include_comment: bool = True,
24
28
  ):
25
29
  """Initialize the question.
26
30
 
27
31
  :param question_name: The name of the question.
28
32
  :param question_text: The text of the question.
29
33
  :param question_options: The options the respondent should select from (list of strings). If not provided, the default Likert options are used (['Strongly disagree', 'Disagree', 'Neutral', 'Agree', 'Strongly agree']). To view them, run `QuestionLikertFive.likert_options`.
30
- :param instructions: Instructions for the question. If not provided, the default instructions are used. To view them, run `QuestionLikertFive.default_instructions`.
31
34
  """
32
35
  super().__init__(
33
36
  question_name=question_name,
34
37
  question_text=question_text,
35
38
  question_options=question_options,
39
+ use_code=False,
40
+ include_comment=include_comment,
41
+ answering_instructions=answering_instructions,
42
+ question_presentation=question_presentation,
36
43
  )
37
44
 
38
- ################
39
- # Helpful
40
- ################
41
45
  @classmethod
46
+ @inject_exception
42
47
  def example(cls) -> QuestionLikertFive:
43
48
  """Return an example question."""
44
49
  return cls(
@@ -4,6 +4,8 @@ from typing import Optional
4
4
  from edsl.questions.descriptors import QuestionOptionsDescriptor, OptionLabelDescriptor
5
5
  from edsl.questions.QuestionMultipleChoice import QuestionMultipleChoice
6
6
 
7
+ from edsl.questions.decorators import inject_exception
8
+
7
9
 
8
10
  class QuestionLinearScale(QuestionMultipleChoice):
9
11
  """This question prompts the agent to respond to a statement on a linear scale."""
@@ -18,6 +20,9 @@ class QuestionLinearScale(QuestionMultipleChoice):
18
20
  question_text: str,
19
21
  question_options: list[int],
20
22
  option_labels: Optional[dict[int, str]] = None,
23
+ answering_instructions: Optional[str] = None,
24
+ question_presentation: Optional[str] = None,
25
+ include_comment: Optional[bool] = True,
21
26
  ):
22
27
  """Instantiate a new QuestionLinearScale.
23
28
 
@@ -31,21 +36,29 @@ class QuestionLinearScale(QuestionMultipleChoice):
31
36
  question_name=question_name,
32
37
  question_text=question_text,
33
38
  question_options=question_options,
39
+ use_code=False, # question linear scale will have it's own code
40
+ include_comment=include_comment,
34
41
  )
35
42
  self.question_options = question_options
36
- self.option_labels = option_labels
43
+ self.option_labels = (
44
+ {int(k): v for k, v in option_labels.items()} if option_labels else {}
45
+ )
46
+ self.answering_instructions = answering_instructions
47
+ self.question_presentation = question_presentation
37
48
 
38
49
  ################
39
50
  # Helpful
40
51
  ################
41
52
  @classmethod
42
- def example(cls) -> QuestionLinearScale:
53
+ @inject_exception
54
+ def example(cls, include_comment: bool = True) -> QuestionLinearScale:
43
55
  """Return an example of a linear scale question."""
44
56
  return cls(
45
57
  question_text="How much do you like ice cream?",
46
58
  question_options=[1, 2, 3, 4, 5],
47
59
  question_name="ice_cream",
48
60
  option_labels={1: "I hate it", 5: "I love it"},
61
+ include_comment=include_comment,
49
62
  )
50
63
 
51
64
 
@@ -3,6 +3,7 @@ from typing import Optional
3
3
 
4
4
  from edsl.exceptions import QuestionCreationValidationError
5
5
  from edsl.questions.QuestionCheckBox import QuestionCheckBox
6
+ from edsl.questions.decorators import inject_exception
6
7
 
7
8
 
8
9
  class QuestionTopK(QuestionCheckBox):
@@ -17,6 +18,9 @@ class QuestionTopK(QuestionCheckBox):
17
18
  question_options: list[str],
18
19
  min_selections: int,
19
20
  max_selections: int,
21
+ question_presentation: Optional[str] = None,
22
+ answering_instructions: Optional[str] = None,
23
+ include_comment: Optional[bool] = True,
20
24
  ):
21
25
  """Initialize the question.
22
26
 
@@ -32,6 +36,9 @@ class QuestionTopK(QuestionCheckBox):
32
36
  question_options=question_options,
33
37
  min_selections=min_selections,
34
38
  max_selections=max_selections,
39
+ question_presentation=question_presentation,
40
+ answering_instructions=answering_instructions,
41
+ include_comment=include_comment,
35
42
  )
36
43
  if min_selections != max_selections:
37
44
  raise QuestionCreationValidationError(
@@ -46,7 +53,8 @@ class QuestionTopK(QuestionCheckBox):
46
53
  # Helpful
47
54
  ################
48
55
  @classmethod
49
- def example(cls) -> QuestionTopK:
56
+ @inject_exception
57
+ def example(cls, include_comment: bool = True) -> QuestionTopK:
50
58
  """Return an example question."""
51
59
  return cls(
52
60
  question_name="two_fruits",
@@ -54,6 +62,7 @@ class QuestionTopK(QuestionCheckBox):
54
62
  question_options=["apple", "banana", "carrot", "durian"],
55
63
  min_selections=2,
56
64
  max_selections=2,
65
+ include_comment=include_comment,
57
66
  )
58
67
 
59
68
 
@@ -1,7 +1,10 @@
1
1
  from __future__ import annotations
2
+ from typing import Optional
2
3
  from edsl.questions.descriptors import QuestionOptionsDescriptor
3
4
  from edsl.questions.QuestionMultipleChoice import QuestionMultipleChoice
4
5
 
6
+ from edsl.questions.decorators import inject_exception
7
+
5
8
 
6
9
  class QuestionYesNo(QuestionMultipleChoice):
7
10
  """This question prompts the agent to respond with 'Yes' or 'No'."""
@@ -13,7 +16,10 @@ class QuestionYesNo(QuestionMultipleChoice):
13
16
  self,
14
17
  question_name: str,
15
18
  question_text: str,
16
- question_options: list[str] = ["Yes", "No"],
19
+ question_options: list[str] = ["No", "Yes"],
20
+ answering_instructions: Optional[str] = None,
21
+ question_presentation: Optional[str] = None,
22
+ include_comment: Optional[bool] = True,
17
23
  ):
18
24
  """Instantiate a new QuestionYesNo.
19
25
 
@@ -25,6 +31,10 @@ class QuestionYesNo(QuestionMultipleChoice):
25
31
  question_name=question_name,
26
32
  question_text=question_text,
27
33
  question_options=question_options,
34
+ use_code=False,
35
+ answering_instructions=answering_instructions,
36
+ question_presentation=question_presentation,
37
+ include_comment=include_comment,
28
38
  )
29
39
  self.question_options = question_options
30
40
 
@@ -32,9 +42,14 @@ class QuestionYesNo(QuestionMultipleChoice):
32
42
  # Helpful
33
43
  ################
34
44
  @classmethod
35
- def example(cls) -> QuestionYesNo:
45
+ @inject_exception
46
+ def example(cls, include_comment: bool = True) -> QuestionYesNo:
36
47
  """Return an example of a yes/no question."""
37
- return cls(question_name="is_it_equal", question_text="Is 5 + 5 equal to 11?")
48
+ return cls(
49
+ question_name="is_it_equal",
50
+ question_text="Is 5 + 5 equal to 11?",
51
+ include_comment=include_comment,
52
+ )
38
53
 
39
54
 
40
55
  def main():
@@ -59,3 +74,9 @@ def main():
59
74
  import doctest
60
75
 
61
76
  doctest.testmod(optionflags=doctest.ELLIPSIS)
77
+
78
+
79
+ if __name__ == "__main__":
80
+ import doctest
81
+
82
+ doctest.testmod(optionflags=doctest.ELLIPSIS)
@@ -206,12 +206,14 @@ class OptionLabelDescriptor(BaseDescriptor):
206
206
 
207
207
  def validate(self, value, instance):
208
208
  """Validate the value is a string."""
209
- if value is not None:
210
- if min(value.keys()) != min(instance.question_options):
209
+ # key_values = [int(v) for v in value.keys()]
210
+
211
+ if value and (key_values := [float(v) for v in value.keys()]) != []:
212
+ if min(key_values) != min(instance.question_options):
211
213
  raise QuestionCreationValidationError(
212
214
  f"First option needs a label (got {value})"
213
215
  )
214
- if max(value.keys()) != max(instance.question_options):
216
+ if max(key_values) != max(instance.question_options):
215
217
  raise QuestionCreationValidationError(
216
218
  f"Last option needs a label (got {value})"
217
219
  )
@@ -219,12 +221,17 @@ class OptionLabelDescriptor(BaseDescriptor):
219
221
  raise QuestionCreationValidationError(
220
222
  "Option labels must be strings (got {value})."
221
223
  )
222
- for key in value.keys():
224
+ for key in key_values:
223
225
  if key not in instance.question_options:
224
226
  raise QuestionCreationValidationError(
225
227
  f"Option label key ({key}) is not in question options ({instance.question_options})."
226
228
  )
227
229
 
230
+ if len(value.values()) != len(set(value.values())):
231
+ raise QuestionCreationValidationError(
232
+ f"Option labels must be unique (got {value})."
233
+ )
234
+
228
235
 
229
236
  class QuestionNameDescriptor(BaseDescriptor):
230
237
  """Validate that the `question_name` attribute is a valid variable name."""
@@ -233,6 +240,15 @@ class QuestionNameDescriptor(BaseDescriptor):
233
240
  """Validate the value is a valid variable name."""
234
241
  from edsl.utilities.utilities import is_valid_variable_name
235
242
 
243
+ if "{{" in value and "}}" in value:
244
+ # they're trying to use a dynamic question name - let's let this play out
245
+ return None
246
+
247
+ if value.endswith("_comment") or value.endswith("_generated_tokens"):
248
+ raise QuestionCreationValidationError(
249
+ f"`question_name` cannot end with '_comment' or '_generated_tokens - (got {value})."
250
+ )
251
+
236
252
  if not is_valid_variable_name(value):
237
253
  raise QuestionCreationValidationError(
238
254
  f"`question_name` is not a valid variable name (got {value})."
@@ -279,7 +295,7 @@ class QuestionOptionsDescriptor(BaseDescriptor):
279
295
  >>> _ = q_class("dynamic_options")
280
296
  Traceback (most recent call last):
281
297
  ...
282
- edsl.exceptions.questions.QuestionCreationValidationError: Dynamic question options must be of the form: '{{ question_options }}'.
298
+ edsl.exceptions.questions.QuestionCreationValidationError: ...
283
299
  """
284
300
  if isinstance(value, str):
285
301
  # Check if the string is a dynamic question option
@@ -287,7 +303,7 @@ class QuestionOptionsDescriptor(BaseDescriptor):
287
303
  return None
288
304
  else:
289
305
  raise QuestionCreationValidationError(
290
- "Dynamic question options must be of the form: '{{ question_options }}'."
306
+ f"Dynamic question options must have jina2 braces - instead received: {value}."
291
307
  )
292
308
  if not isinstance(value, list):
293
309
  raise QuestionCreationValidationError(
@@ -356,7 +372,21 @@ class QuestionOptionsDescriptor(BaseDescriptor):
356
372
 
357
373
 
358
374
  class QuestionTextDescriptor(BaseDescriptor):
359
- """Validate that the `question_text` attribute is a string."""
375
+ """Validate that the `question_text` attribute is a string.
376
+
377
+
378
+ >>> class TestQuestion:
379
+ ... question_text = QuestionTextDescriptor()
380
+ ... def __init__(self, question_text: str):
381
+ ... self.question_text = question_text
382
+
383
+ >>> _ = TestQuestion("What is the capital of France?")
384
+ >>> _ = TestQuestion("What is the capital of France? {{variable}}")
385
+ >>> _ = TestQuestion("What is the capital of France? {{variable name}}")
386
+ Traceback (most recent call last):
387
+ ...
388
+ edsl.exceptions.questions.QuestionCreationValidationError: Question text contains an invalid identifier: 'variable name'
389
+ """
360
390
 
361
391
  def validate(self, value, instance):
362
392
  """Validate the value is a string."""
@@ -373,6 +403,12 @@ class QuestionTextDescriptor(BaseDescriptor):
373
403
  f"WARNING: Question text contains a single-braced substring: If you intended to parameterize the question with a Scenario this should be changed to a double-braced substring, e.g. {{variable}}.\nSee details on constructing Scenarios in the docs: https://docs.expectedparrot.com/en/latest/scenarios.html",
374
404
  UserWarning,
375
405
  )
406
+ # iterate through all doubles braces and check if they are valid python identifiers
407
+ for match in re.finditer(r"\{\{([^\{\}]+)\}\}", value):
408
+ if " " in match.group(1).strip():
409
+ raise QuestionCreationValidationError(
410
+ f"Question text contains an invalid identifier: '{match.group(1)}'"
411
+ )
376
412
 
377
413
 
378
414
  if __name__ == "__main__":
@@ -0,0 +1,13 @@
1
+ You are being asked the following question: {{question_text}}
2
+ The options are:
3
+ {% for option in question_options %}
4
+ {{ loop.index0 }}: {{option}}
5
+ {% endfor %}
6
+ Return a valid JSON formatted as follows, with a dictionary for your "answer"
7
+ where the keys are the option numbers and the values are the amounts you want
8
+ to allocate to the options, and the sum of the values is {{budget_sum}}:
9
+
10
+ {"answer": {<put dict of option numbers and allocation amounts here>}, "comment": "<put explanation here>"}
11
+ Example response for a budget of 100 and 4 options:
12
+ {"answer": {"0": 25, "1": 25, "2": 25, "3": 25}, "comment": "I allocated 25 to each option."}
13
+ There must be an allocation listed for each item (including 0).
@@ -0,0 +1,32 @@
1
+ {# Question Presention #}
2
+ {{question_text}}
3
+ {% if use_code %}
4
+ {% for option in question_options %}
5
+ {{ loop.index0 }}: {{option}}
6
+ {% endfor %}
7
+ {% else %}
8
+ {% for option in question_options %}
9
+ {{ option }}
10
+ {% endfor %}
11
+ {% endif %}
12
+
13
+ {# Restrictions #}
14
+ {% if min_selections != None and max_selections != None and min_selections == max_selections %}
15
+ You must select exactly {{min_selections}} options.
16
+ {% elif min_selections != None and max_selections != None %}
17
+ Minimum number of options that must be selected: {{min_selections}}.
18
+ Maximum number of options that must be selected: {{max_selections}}.
19
+ {% elif min_selections != None %}
20
+ Minimum number of options that must be selected: {{min_selections}}.
21
+ {% elif max_selections != None %}
22
+ Maximum number of options that must be selected: {{max_selections}}.
23
+ {% endif %}
24
+
25
+ {# Answering Instructions #}
26
+ Please respond with valid JSON, formatted like so:
27
+ {% if include_comment %}
28
+ {"answer": [<put comma-separated list here>], "comment": "<put explanation here>"}
29
+ {% else %}
30
+ {"answer": [<put comma-separated list here>]}
31
+ {% endif %}
32
+
@@ -0,0 +1,11 @@
1
+ {{question_text}}
2
+
3
+ Create an ANSWER should be formatted like this:
4
+ {{ answer_template }}
5
+
6
+ It should have the same keys but values extracted from the input.
7
+ If the value of a key is not present in the input, fill with "null".
8
+
9
+ Return a valid JSON formatted like this:
10
+ {"answer": <put your ANSWER here>}
11
+ ONLY RETURN THE JSON, AND NOTHING ELSE.
@@ -0,0 +1,3 @@
1
+ {{question_text}}
2
+ Return a valid JSON formatted like this:
3
+ {"answer": "<put free text answer here>"}
@@ -0,0 +1,11 @@
1
+ {{question_text}}
2
+ {% for option in question_options %}
3
+ {{option}} : {{ option_labels.get(option, "") }}
4
+ {% endfor %}
5
+ Return a valid JSON formatted like this, selecting only the code of the option (codes start at 0):
6
+ {% if include_comment %}
7
+ {"answer": <put answer code here>, "comment": <comment>}
8
+ {% else %}
9
+ {"answer": <put answer here>}
10
+ {% endif %}
11
+ Only 1 option may be selected.
@@ -0,0 +1,17 @@
1
+ {{question_text}}
2
+
3
+ Your response should be only a valid JSON in the following format:
4
+ {% if include_comment %}
5
+ {
6
+ "answer": [<comma-separated list of responsive words or phrases as independent strings>],
7
+ "comment": "<put comment here>"
8
+ }
9
+ {% else %}
10
+ {
11
+ "answer": [<comma-separated list of responsive words or phrases as independent strings>],
12
+ }
13
+ {% endif %}
14
+
15
+ {% if max_list_items is not none %}
16
+ The list must not contain more than {{ max_list_items }} items.
17
+ {% endif %}
@@ -0,0 +1,33 @@
1
+ {# Question Presention #}
2
+ {{question_text}}
3
+
4
+ {% if use_code %}
5
+ {% for option in question_options %}
6
+ {{ loop.index0 }}: {{option}}
7
+ {% endfor %}
8
+ {% else %}
9
+ {% for option in question_options %}
10
+ {{option}}
11
+ {% endfor %}
12
+ {% endif %}
13
+
14
+ Only 1 option may be selected.
15
+
16
+ {# Answering Instructions #}
17
+ Return a valid JSON formatted like this:
18
+
19
+ {% if use_code %}
20
+ {% if include_comment %}
21
+ {"answer": <put answer code here>, "comment": "<put explanation here>"}
22
+ {% else %}
23
+ {"answer": <put answer code here>}
24
+ {% endif %}
25
+ {% else %}
26
+
27
+ {% if include_comment %}
28
+ {"answer": <text of option>, "comment": "<put explanation here>"}
29
+ {% else %}
30
+ {"answer": <put option here>}
31
+ {% endif %}
32
+
33
+ {% endif %}