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.
- edsl/Base.py +9 -3
- edsl/TemplateLoader.py +24 -0
- edsl/__init__.py +8 -3
- edsl/__version__.py +1 -1
- edsl/agents/Agent.py +40 -8
- edsl/agents/AgentList.py +43 -0
- edsl/agents/Invigilator.py +135 -219
- edsl/agents/InvigilatorBase.py +148 -59
- edsl/agents/{PromptConstructionMixin.py → PromptConstructor.py} +138 -89
- edsl/agents/__init__.py +1 -0
- edsl/auto/AutoStudy.py +117 -0
- edsl/auto/StageBase.py +230 -0
- edsl/auto/StageGenerateSurvey.py +178 -0
- edsl/auto/StageLabelQuestions.py +125 -0
- edsl/auto/StagePersona.py +61 -0
- edsl/auto/StagePersonaDimensionValueRanges.py +88 -0
- edsl/auto/StagePersonaDimensionValues.py +74 -0
- edsl/auto/StagePersonaDimensions.py +69 -0
- edsl/auto/StageQuestions.py +73 -0
- edsl/auto/SurveyCreatorPipeline.py +21 -0
- edsl/auto/utilities.py +224 -0
- edsl/config.py +47 -56
- edsl/coop/PriceFetcher.py +58 -0
- edsl/coop/coop.py +50 -7
- edsl/data/Cache.py +35 -1
- edsl/data_transfer_models.py +73 -38
- edsl/enums.py +4 -0
- edsl/exceptions/language_models.py +25 -1
- edsl/exceptions/questions.py +62 -5
- edsl/exceptions/results.py +4 -0
- edsl/inference_services/AnthropicService.py +13 -11
- edsl/inference_services/AwsBedrock.py +19 -17
- edsl/inference_services/AzureAI.py +37 -20
- edsl/inference_services/GoogleService.py +16 -12
- edsl/inference_services/GroqService.py +2 -0
- edsl/inference_services/InferenceServiceABC.py +58 -3
- edsl/inference_services/MistralAIService.py +120 -0
- edsl/inference_services/OpenAIService.py +48 -54
- edsl/inference_services/TestService.py +80 -0
- edsl/inference_services/TogetherAIService.py +170 -0
- edsl/inference_services/models_available_cache.py +0 -6
- edsl/inference_services/registry.py +6 -0
- edsl/jobs/Answers.py +10 -12
- edsl/jobs/FailedQuestion.py +78 -0
- edsl/jobs/Jobs.py +37 -22
- edsl/jobs/buckets/BucketCollection.py +24 -15
- edsl/jobs/buckets/TokenBucket.py +93 -14
- edsl/jobs/interviews/Interview.py +366 -78
- edsl/jobs/interviews/{interview_exception_tracking.py → InterviewExceptionCollection.py} +14 -68
- edsl/jobs/interviews/InterviewExceptionEntry.py +85 -19
- edsl/jobs/runners/JobsRunnerAsyncio.py +146 -175
- edsl/jobs/runners/JobsRunnerStatus.py +331 -0
- edsl/jobs/tasks/QuestionTaskCreator.py +30 -23
- edsl/jobs/tasks/TaskHistory.py +148 -213
- edsl/language_models/LanguageModel.py +261 -156
- edsl/language_models/ModelList.py +2 -2
- edsl/language_models/RegisterLanguageModelsMeta.py +14 -29
- edsl/language_models/fake_openai_call.py +15 -0
- edsl/language_models/fake_openai_service.py +61 -0
- edsl/language_models/registry.py +23 -6
- edsl/language_models/repair.py +0 -19
- edsl/language_models/utilities.py +61 -0
- edsl/notebooks/Notebook.py +20 -2
- edsl/prompts/Prompt.py +52 -2
- edsl/questions/AnswerValidatorMixin.py +23 -26
- edsl/questions/QuestionBase.py +330 -249
- edsl/questions/QuestionBaseGenMixin.py +133 -0
- edsl/questions/QuestionBasePromptsMixin.py +266 -0
- edsl/questions/QuestionBudget.py +99 -41
- edsl/questions/QuestionCheckBox.py +227 -35
- edsl/questions/QuestionExtract.py +98 -27
- edsl/questions/QuestionFreeText.py +52 -29
- edsl/questions/QuestionFunctional.py +7 -0
- edsl/questions/QuestionList.py +141 -22
- edsl/questions/QuestionMultipleChoice.py +159 -65
- edsl/questions/QuestionNumerical.py +88 -46
- edsl/questions/QuestionRank.py +182 -24
- edsl/questions/Quick.py +41 -0
- edsl/questions/RegisterQuestionsMeta.py +31 -12
- edsl/questions/ResponseValidatorABC.py +170 -0
- edsl/questions/__init__.py +3 -4
- edsl/questions/decorators.py +21 -0
- edsl/questions/derived/QuestionLikertFive.py +10 -5
- edsl/questions/derived/QuestionLinearScale.py +15 -2
- edsl/questions/derived/QuestionTopK.py +10 -1
- edsl/questions/derived/QuestionYesNo.py +24 -3
- edsl/questions/descriptors.py +43 -7
- edsl/questions/prompt_templates/question_budget.jinja +13 -0
- edsl/questions/prompt_templates/question_checkbox.jinja +32 -0
- edsl/questions/prompt_templates/question_extract.jinja +11 -0
- edsl/questions/prompt_templates/question_free_text.jinja +3 -0
- edsl/questions/prompt_templates/question_linear_scale.jinja +11 -0
- edsl/questions/prompt_templates/question_list.jinja +17 -0
- edsl/questions/prompt_templates/question_multiple_choice.jinja +33 -0
- edsl/questions/prompt_templates/question_numerical.jinja +37 -0
- edsl/questions/question_registry.py +6 -2
- edsl/questions/templates/__init__.py +0 -0
- edsl/questions/templates/budget/__init__.py +0 -0
- edsl/questions/templates/budget/answering_instructions.jinja +7 -0
- edsl/questions/templates/budget/question_presentation.jinja +7 -0
- edsl/questions/templates/checkbox/__init__.py +0 -0
- edsl/questions/templates/checkbox/answering_instructions.jinja +10 -0
- edsl/questions/templates/checkbox/question_presentation.jinja +22 -0
- edsl/questions/templates/extract/__init__.py +0 -0
- edsl/questions/templates/extract/answering_instructions.jinja +7 -0
- edsl/questions/templates/extract/question_presentation.jinja +1 -0
- edsl/questions/templates/free_text/__init__.py +0 -0
- edsl/questions/templates/free_text/answering_instructions.jinja +0 -0
- edsl/questions/templates/free_text/question_presentation.jinja +1 -0
- edsl/questions/templates/likert_five/__init__.py +0 -0
- edsl/questions/templates/likert_five/answering_instructions.jinja +10 -0
- edsl/questions/templates/likert_five/question_presentation.jinja +12 -0
- edsl/questions/templates/linear_scale/__init__.py +0 -0
- edsl/questions/templates/linear_scale/answering_instructions.jinja +5 -0
- edsl/questions/templates/linear_scale/question_presentation.jinja +5 -0
- edsl/questions/templates/list/__init__.py +0 -0
- edsl/questions/templates/list/answering_instructions.jinja +4 -0
- edsl/questions/templates/list/question_presentation.jinja +5 -0
- edsl/questions/templates/multiple_choice/__init__.py +0 -0
- edsl/questions/templates/multiple_choice/answering_instructions.jinja +9 -0
- edsl/questions/templates/multiple_choice/html.jinja +0 -0
- edsl/questions/templates/multiple_choice/question_presentation.jinja +12 -0
- edsl/questions/templates/numerical/__init__.py +0 -0
- edsl/questions/templates/numerical/answering_instructions.jinja +8 -0
- edsl/questions/templates/numerical/question_presentation.jinja +7 -0
- edsl/questions/templates/rank/__init__.py +0 -0
- edsl/questions/templates/rank/answering_instructions.jinja +11 -0
- edsl/questions/templates/rank/question_presentation.jinja +15 -0
- edsl/questions/templates/top_k/__init__.py +0 -0
- edsl/questions/templates/top_k/answering_instructions.jinja +8 -0
- edsl/questions/templates/top_k/question_presentation.jinja +22 -0
- edsl/questions/templates/yes_no/__init__.py +0 -0
- edsl/questions/templates/yes_no/answering_instructions.jinja +6 -0
- edsl/questions/templates/yes_no/question_presentation.jinja +12 -0
- edsl/results/Dataset.py +20 -0
- edsl/results/DatasetExportMixin.py +46 -48
- edsl/results/DatasetTree.py +145 -0
- edsl/results/Result.py +32 -5
- edsl/results/Results.py +135 -46
- edsl/results/ResultsDBMixin.py +3 -3
- edsl/results/Selector.py +118 -0
- edsl/results/tree_explore.py +115 -0
- edsl/scenarios/FileStore.py +71 -10
- edsl/scenarios/Scenario.py +96 -25
- edsl/scenarios/ScenarioImageMixin.py +2 -2
- edsl/scenarios/ScenarioList.py +361 -39
- edsl/scenarios/ScenarioListExportMixin.py +9 -0
- edsl/scenarios/ScenarioListPdfMixin.py +150 -4
- edsl/study/SnapShot.py +8 -1
- edsl/study/Study.py +32 -0
- edsl/surveys/Rule.py +10 -1
- edsl/surveys/RuleCollection.py +21 -5
- edsl/surveys/Survey.py +637 -311
- edsl/surveys/SurveyExportMixin.py +71 -9
- edsl/surveys/SurveyFlowVisualizationMixin.py +2 -1
- edsl/surveys/SurveyQualtricsImport.py +75 -4
- edsl/surveys/instructions/ChangeInstruction.py +47 -0
- edsl/surveys/instructions/Instruction.py +34 -0
- edsl/surveys/instructions/InstructionCollection.py +77 -0
- edsl/surveys/instructions/__init__.py +0 -0
- edsl/templates/error_reporting/base.html +24 -0
- edsl/templates/error_reporting/exceptions_by_model.html +35 -0
- edsl/templates/error_reporting/exceptions_by_question_name.html +17 -0
- edsl/templates/error_reporting/exceptions_by_type.html +17 -0
- edsl/templates/error_reporting/interview_details.html +116 -0
- edsl/templates/error_reporting/interviews.html +10 -0
- edsl/templates/error_reporting/overview.html +5 -0
- edsl/templates/error_reporting/performance_plot.html +2 -0
- edsl/templates/error_reporting/report.css +74 -0
- edsl/templates/error_reporting/report.html +118 -0
- edsl/templates/error_reporting/report.js +25 -0
- edsl/utilities/utilities.py +9 -1
- {edsl-0.1.32.dist-info → edsl-0.1.33.dist-info}/METADATA +5 -2
- edsl-0.1.33.dist-info/RECORD +295 -0
- edsl/jobs/interviews/InterviewTaskBuildingMixin.py +0 -286
- edsl/jobs/interviews/retry_management.py +0 -37
- edsl/jobs/runners/JobsRunnerStatusMixin.py +0 -333
- edsl/utilities/gcp_bucket/simple_example.py +0 -9
- edsl-0.1.32.dist-info/RECORD +0 -209
- {edsl-0.1.32.dist-info → edsl-0.1.33.dist-info}/LICENSE +0 -0
- {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)
|
edsl/questions/__init__.py
CHANGED
@@ -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 =
|
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
|
-
|
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
|
-
|
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] = ["
|
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
|
-
|
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(
|
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)
|
edsl/questions/descriptors.py
CHANGED
@@ -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
|
-
|
210
|
-
|
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(
|
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
|
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:
|
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
|
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,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 %}
|