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
edsl/questions/QuestionList.py
CHANGED
@@ -4,6 +4,123 @@ import textwrap
|
|
4
4
|
from typing import Any, Optional, Union
|
5
5
|
from edsl.questions.QuestionBase import QuestionBase
|
6
6
|
from edsl.questions.descriptors import IntegerOrNoneDescriptor
|
7
|
+
from edsl.questions.decorators import inject_exception
|
8
|
+
|
9
|
+
from pydantic import field_validator, Field
|
10
|
+
from edsl.questions.ResponseValidatorABC import ResponseValidatorABC
|
11
|
+
from edsl.questions.ResponseValidatorABC import BaseResponse
|
12
|
+
|
13
|
+
from edsl.exceptions import QuestionAnswerValidationError
|
14
|
+
import textwrap
|
15
|
+
import json
|
16
|
+
|
17
|
+
from json_repair import repair_json
|
18
|
+
|
19
|
+
|
20
|
+
def convert_string(s):
|
21
|
+
"""Convert a string to a more appropriate type if possible.
|
22
|
+
|
23
|
+
>>> convert_string("3.14")
|
24
|
+
3.14
|
25
|
+
>>> convert_string("42")
|
26
|
+
42
|
27
|
+
>>> convert_string("hello")
|
28
|
+
'hello'
|
29
|
+
>>> convert_string('{"key": "value"}')
|
30
|
+
{'key': 'value'}
|
31
|
+
>>> convert_string("{'key': 'value'}")
|
32
|
+
{'key': 'value'}
|
33
|
+
"""
|
34
|
+
|
35
|
+
if not isinstance(s, str): # if it's not a string, return it as is
|
36
|
+
return s
|
37
|
+
|
38
|
+
# If the repair returns, continue on; otherwise, try to load it as JSON
|
39
|
+
if (repaired_json := repair_json(s)) == '""':
|
40
|
+
pass
|
41
|
+
else:
|
42
|
+
try:
|
43
|
+
return json.loads(repaired_json)
|
44
|
+
except json.JSONDecodeError:
|
45
|
+
pass
|
46
|
+
|
47
|
+
# Try to convert to float
|
48
|
+
try:
|
49
|
+
return float(s)
|
50
|
+
except ValueError:
|
51
|
+
pass
|
52
|
+
|
53
|
+
# Try to convert to int
|
54
|
+
try:
|
55
|
+
return int(s)
|
56
|
+
except ValueError:
|
57
|
+
pass
|
58
|
+
|
59
|
+
# If all conversions fail, return the original string
|
60
|
+
return s
|
61
|
+
|
62
|
+
|
63
|
+
def create_model(max_list_items: int, permissive):
|
64
|
+
from pydantic import BaseModel
|
65
|
+
|
66
|
+
if permissive or max_list_items is None:
|
67
|
+
|
68
|
+
class ListResponse(BaseModel):
|
69
|
+
answer: list[Any]
|
70
|
+
comment: Optional[str] = None
|
71
|
+
generated_tokens: Optional[str] = None
|
72
|
+
|
73
|
+
else:
|
74
|
+
|
75
|
+
class ListResponse(BaseModel):
|
76
|
+
"""
|
77
|
+
>>> nr = ListResponse(answer=["Apple", "Cherry"])
|
78
|
+
>>> nr.dict()
|
79
|
+
{'answer': ['Apple', 'Cherry'], 'comment': None, 'generated_tokens': None}
|
80
|
+
"""
|
81
|
+
|
82
|
+
answer: list[Any] = Field(..., min_items=0, max_items=max_list_items)
|
83
|
+
comment: Optional[str] = None
|
84
|
+
generated_tokens: Optional[str] = None
|
85
|
+
|
86
|
+
return ListResponse
|
87
|
+
|
88
|
+
|
89
|
+
class ListResponseValidator(ResponseValidatorABC):
|
90
|
+
required_params = ["max_list_items", "permissive"]
|
91
|
+
valid_examples = [({"answer": ["hello", "world"]}, {"max_list_items": 5})]
|
92
|
+
|
93
|
+
invalid_examples = [
|
94
|
+
(
|
95
|
+
{"answer": ["hello", "world", "this", "is", "a", "test"]},
|
96
|
+
{"max_list_items": 5},
|
97
|
+
"Too many items.",
|
98
|
+
),
|
99
|
+
]
|
100
|
+
|
101
|
+
def _check_constraints(self, response) -> None:
|
102
|
+
if (
|
103
|
+
self.max_list_items is not None
|
104
|
+
and len(response.answer) > self.max_list_items
|
105
|
+
):
|
106
|
+
raise QuestionAnswerValidationError("Too many items.")
|
107
|
+
|
108
|
+
def fix(self, response, verbose=False):
|
109
|
+
if verbose:
|
110
|
+
print(f"Fixing list response: {response}")
|
111
|
+
answer = str(response.get("answer") or response.get("generated_tokens", ""))
|
112
|
+
if len(answer.split(",")) > 0:
|
113
|
+
return (
|
114
|
+
{"answer": answer.split(",")} | {"comment": response.get("comment")}
|
115
|
+
if "comment" in response
|
116
|
+
else {}
|
117
|
+
)
|
118
|
+
|
119
|
+
def _post_process(self, edsl_answer_dict):
|
120
|
+
edsl_answer_dict["answer"] = [
|
121
|
+
convert_string(item) for item in edsl_answer_dict["answer"]
|
122
|
+
]
|
123
|
+
return edsl_answer_dict
|
7
124
|
|
8
125
|
|
9
126
|
class QuestionList(QuestionBase):
|
@@ -11,43 +128,38 @@ class QuestionList(QuestionBase):
|
|
11
128
|
|
12
129
|
question_type = "list"
|
13
130
|
max_list_items: int = IntegerOrNoneDescriptor()
|
131
|
+
_response_model = None
|
132
|
+
response_validator_class = ListResponseValidator
|
14
133
|
|
15
134
|
def __init__(
|
16
135
|
self,
|
17
136
|
question_name: str,
|
18
137
|
question_text: str,
|
19
138
|
max_list_items: Optional[int] = None,
|
139
|
+
include_comment: bool = True,
|
140
|
+
answering_instructions: Optional[str] = None,
|
141
|
+
question_presentation: Optional[str] = None,
|
142
|
+
permissive: bool = False,
|
20
143
|
):
|
21
144
|
"""Instantiate a new QuestionList.
|
22
145
|
|
23
146
|
:param question_name: The name of the question.
|
24
147
|
:param question_text: The text of the question.
|
25
148
|
:param max_list_items: The maximum number of items that can be in the answer list.
|
149
|
+
|
150
|
+
>>> QuestionList.example().self_check()
|
26
151
|
"""
|
27
152
|
self.question_name = question_name
|
28
153
|
self.question_text = question_text
|
29
154
|
self.max_list_items = max_list_items
|
155
|
+
self.permissive = permissive
|
30
156
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
def _validate_answer(self, answer: Any) -> dict[str, Union[list[str], str]]:
|
35
|
-
"""Validate the answer."""
|
36
|
-
self._validate_answer_template_basic(answer)
|
37
|
-
self._validate_answer_key_value(answer, "answer", list)
|
38
|
-
self._validate_answer_list(answer)
|
39
|
-
return answer
|
40
|
-
|
41
|
-
def _translate_answer_code_to_answer(self, answer, scenario: "Scenario" = None):
|
42
|
-
"""There is no answer code."""
|
43
|
-
return answer
|
44
|
-
|
45
|
-
def _simulate_answer(self, human_readable: bool = True):
|
46
|
-
"""Simulate a valid answer for debugging purposes (what the validator expects)."""
|
47
|
-
num_items = random.randint(1, self.max_list_items or 2)
|
48
|
-
from edsl.utilities.utilities import random_string
|
157
|
+
self.include_comment = include_comment
|
158
|
+
self.answering_instructions = answering_instructions
|
159
|
+
self.question_presentations = question_presentation
|
49
160
|
|
50
|
-
|
161
|
+
def create_response_model(self):
|
162
|
+
return create_model(self.max_list_items, self.permissive)
|
51
163
|
|
52
164
|
@property
|
53
165
|
def question_html_content(self) -> str:
|
@@ -78,12 +190,17 @@ class QuestionList(QuestionBase):
|
|
78
190
|
# Helpful methods
|
79
191
|
################
|
80
192
|
@classmethod
|
81
|
-
|
193
|
+
@inject_exception
|
194
|
+
def example(
|
195
|
+
cls, include_comment=True, max_list_items=None, permissive=False
|
196
|
+
) -> QuestionList:
|
82
197
|
"""Return an example of a list question."""
|
83
198
|
return cls(
|
84
199
|
question_name="list_of_foods",
|
85
200
|
question_text="What are your favorite foods?",
|
86
|
-
|
201
|
+
include_comment=include_comment,
|
202
|
+
max_list_items=max_list_items,
|
203
|
+
permissive=permissive,
|
87
204
|
)
|
88
205
|
|
89
206
|
|
@@ -91,7 +208,7 @@ def main():
|
|
91
208
|
"""Create an example of a list question and demonstrate its functionality."""
|
92
209
|
from edsl.questions.QuestionList import QuestionList
|
93
210
|
|
94
|
-
q = QuestionList.example()
|
211
|
+
q = QuestionList.example(max_list_items=5)
|
95
212
|
q.question_text
|
96
213
|
q.question_name
|
97
214
|
q.max_list_items
|
@@ -107,6 +224,8 @@ def main():
|
|
107
224
|
q.to_dict()
|
108
225
|
assert q.from_dict(q.to_dict()) == q
|
109
226
|
|
227
|
+
|
228
|
+
if __name__ == "__main__":
|
110
229
|
import doctest
|
111
230
|
|
112
231
|
doctest.testmod(optionflags=doctest.ELLIPSIS)
|
@@ -1,12 +1,114 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
-
import
|
3
|
-
|
4
|
-
import random
|
5
|
-
from typing import Optional
|
2
|
+
from typing import Union, Literal, Optional, List, Any
|
3
|
+
|
6
4
|
from jinja2 import Template
|
5
|
+
from pydantic import BaseModel, Field
|
7
6
|
|
7
|
+
from edsl.scenarios.Scenario import Scenario
|
8
8
|
from edsl.questions.QuestionBase import QuestionBase
|
9
9
|
from edsl.questions.descriptors import QuestionOptionsDescriptor
|
10
|
+
from edsl.questions.decorators import inject_exception
|
11
|
+
from edsl.questions.ResponseValidatorABC import ResponseValidatorABC
|
12
|
+
|
13
|
+
|
14
|
+
def create_response_model(choices: List[str], permissive: bool = False):
|
15
|
+
"""
|
16
|
+
Create a ChoiceResponse model class with a predefined list of choices.
|
17
|
+
|
18
|
+
:param choices: A list of allowed values for the answer field.
|
19
|
+
:param permissive: If True, any value will be accepted as an answer.
|
20
|
+
:return: A new Pydantic model class.
|
21
|
+
"""
|
22
|
+
choice_tuple = tuple(choices)
|
23
|
+
|
24
|
+
if not permissive:
|
25
|
+
|
26
|
+
class ChoiceResponse(BaseModel):
|
27
|
+
answer: Literal[choice_tuple] = Field(description="Selected choice")
|
28
|
+
comment: Optional[str] = Field(None, description="Optional comment field")
|
29
|
+
generated_tokens: Optional[Any] = Field(
|
30
|
+
None, description="Generated tokens"
|
31
|
+
)
|
32
|
+
|
33
|
+
class Config:
|
34
|
+
@staticmethod
|
35
|
+
def json_schema_extra(schema: dict, model: BaseModel) -> None:
|
36
|
+
for prop in schema.get("properties", {}).values():
|
37
|
+
if prop.get("title") == "answer":
|
38
|
+
prop["enum"] = choices
|
39
|
+
|
40
|
+
else:
|
41
|
+
|
42
|
+
class ChoiceResponse(BaseModel):
|
43
|
+
answer: Any = Field(description="Selected choice (can be any value)")
|
44
|
+
comment: Optional[str] = Field(None, description="Optional comment field")
|
45
|
+
generated_tokens: Optional[Any] = Field(
|
46
|
+
None, description="Generated tokens"
|
47
|
+
)
|
48
|
+
|
49
|
+
class Config:
|
50
|
+
@staticmethod
|
51
|
+
def json_schema_extra(schema: dict, model: BaseModel) -> None:
|
52
|
+
for prop in schema.get("properties", {}).values():
|
53
|
+
if prop.get("title") == "answer":
|
54
|
+
prop["description"] += f". Suggested choices are: {choices}"
|
55
|
+
schema["title"] += " (Permissive)"
|
56
|
+
|
57
|
+
return ChoiceResponse
|
58
|
+
|
59
|
+
|
60
|
+
class MultipleChoiceResponseValidator(ResponseValidatorABC):
|
61
|
+
required_params = ["question_options", "use_code"]
|
62
|
+
|
63
|
+
def fix(self, response, verbose=False):
|
64
|
+
response_text = str(response.get("answer"))
|
65
|
+
if response_text is None:
|
66
|
+
response_text = response.get("generated_tokens", "")
|
67
|
+
|
68
|
+
if verbose:
|
69
|
+
print(f"Invalid generated tokens was: {response_text}")
|
70
|
+
|
71
|
+
matches = []
|
72
|
+
for idx, option in enumerate(self.question_options):
|
73
|
+
if verbose:
|
74
|
+
print("The options are: ", self.question_options)
|
75
|
+
if str(option) in response_text:
|
76
|
+
if verbose:
|
77
|
+
print("Match found with option ", option)
|
78
|
+
if option not in matches:
|
79
|
+
matches.append(option)
|
80
|
+
|
81
|
+
if verbose:
|
82
|
+
print("The matches are: ", matches)
|
83
|
+
if len(matches) == 1:
|
84
|
+
proposed_data = {
|
85
|
+
"answer": matches[0],
|
86
|
+
"generated_tokens": response.get("generated_tokens", None),
|
87
|
+
}
|
88
|
+
try:
|
89
|
+
self.response_model(**proposed_data)
|
90
|
+
return proposed_data
|
91
|
+
except Exception as e:
|
92
|
+
if verbose:
|
93
|
+
print(f"Proposed solution {proposed_data} is invalid. Error: {e}")
|
94
|
+
return response
|
95
|
+
|
96
|
+
valid_examples = [
|
97
|
+
({"answer": 1}, {"question_options": ["Good", "Great", "OK", "Bad"]})
|
98
|
+
]
|
99
|
+
|
100
|
+
invalid_examples = [
|
101
|
+
(
|
102
|
+
{"answer": -1},
|
103
|
+
{"question_options": ["Good", "Great", "OK", "Bad"]},
|
104
|
+
"Answer code must be a non-negative integer",
|
105
|
+
),
|
106
|
+
(
|
107
|
+
{"answer": None},
|
108
|
+
{"question_options": ["Good", "Great", "OK", "Bad"]},
|
109
|
+
"Answer code must not be missing.",
|
110
|
+
),
|
111
|
+
]
|
10
112
|
|
11
113
|
|
12
114
|
class QuestionMultipleChoice(QuestionBase):
|
@@ -21,49 +123,53 @@ class QuestionMultipleChoice(QuestionBase):
|
|
21
123
|
question_options: Union[
|
22
124
|
list[str], list[list], list[float], list[int]
|
23
125
|
] = QuestionOptionsDescriptor()
|
126
|
+
_response_model = None
|
127
|
+
response_validator_class = MultipleChoiceResponseValidator
|
24
128
|
|
25
129
|
def __init__(
|
26
130
|
self,
|
27
131
|
question_name: str,
|
28
132
|
question_text: str,
|
29
133
|
question_options: Union[list[str], list[list], list[float], list[int]],
|
134
|
+
include_comment: bool = True,
|
135
|
+
use_code: bool = False,
|
136
|
+
answering_instructions: Optional[str] = None,
|
137
|
+
question_presentation: Optional[str] = None,
|
138
|
+
permissive: bool = False,
|
30
139
|
):
|
31
140
|
"""Instantiate a new QuestionMultipleChoice.
|
32
141
|
|
33
142
|
:param question_name: The name of the question.
|
34
143
|
:param question_text: The text of the question.
|
35
144
|
:param question_options: The options the agent should select from.
|
145
|
+
:param include_comment: Whether to include a comment field.
|
146
|
+
:param use_code: Whether to use code for the options.
|
147
|
+
:param answering_instructions: Instructions for the question.
|
148
|
+
:param question_presentation: The presentation of the question.
|
149
|
+
:param permissive: Whether to force the answer to be one of the options.
|
150
|
+
|
36
151
|
"""
|
37
152
|
self.question_name = question_name
|
38
153
|
self.question_text = question_text
|
39
154
|
self.question_options = question_options
|
40
155
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
156
|
+
self._include_comment = include_comment
|
157
|
+
self.use_code = use_code
|
158
|
+
self.answering_instructions = answering_instructions
|
159
|
+
self.question_presentation = question_presentation
|
160
|
+
self.permissive = permissive
|
45
161
|
|
46
162
|
################
|
47
163
|
# Answer methods
|
48
164
|
################
|
49
|
-
def _validate_answer(
|
50
|
-
self, answer: dict[str, Union[str, int]]
|
51
|
-
) -> dict[str, Union[str, int]]:
|
52
|
-
"""Validate the answer.
|
53
165
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
...
|
62
|
-
edsl.exceptions.questions.QuestionAnswerValidationError: Answer code must be a non-negative integer (got -1).
|
63
|
-
"""
|
64
|
-
self._validate_answer_template_basic(answer)
|
65
|
-
self._validate_answer_multiple_choice(answer)
|
66
|
-
return answer
|
166
|
+
def create_response_model(self):
|
167
|
+
if self.use_code:
|
168
|
+
return create_response_model(
|
169
|
+
list(range(len(self.question_options))), self.permissive
|
170
|
+
)
|
171
|
+
else:
|
172
|
+
return create_response_model(self.question_options, self.permissive)
|
67
173
|
|
68
174
|
def _translate_answer_code_to_answer(
|
69
175
|
self, answer_code: int, scenario: Optional["Scenario"] = None
|
@@ -74,15 +180,14 @@ class QuestionMultipleChoice(QuestionBase):
|
|
74
180
|
The question options might be templates, so they need to be rendered with the scenario.
|
75
181
|
|
76
182
|
>>> q = QuestionMultipleChoice.example()
|
77
|
-
>>> q._translate_answer_code_to_answer(
|
183
|
+
>>> q._translate_answer_code_to_answer('Good', {})
|
78
184
|
'Good'
|
79
185
|
|
80
186
|
>>> q = QuestionMultipleChoice(question_name="how_feeling", question_text="How are you?", question_options=["{{emotion[0]}}", "emotion[1]"])
|
81
|
-
>>> q._translate_answer_code_to_answer(
|
187
|
+
>>> q._translate_answer_code_to_answer('Happy', {"emotion": ["Happy", "Sad"]})
|
82
188
|
'Happy'
|
83
189
|
|
84
190
|
"""
|
85
|
-
from edsl.scenarios.Scenario import Scenario
|
86
191
|
|
87
192
|
scenario = scenario or Scenario()
|
88
193
|
|
@@ -95,31 +200,17 @@ class QuestionMultipleChoice(QuestionBase):
|
|
95
200
|
question_option_key = list(meta.find_undeclared_variables(parsed_content))[
|
96
201
|
0
|
97
202
|
]
|
98
|
-
# breakpoint()
|
99
203
|
translated_options = scenario.get(question_option_key)
|
100
204
|
else:
|
101
205
|
translated_options = [
|
102
206
|
Template(str(option)).render(scenario)
|
103
207
|
for option in self.question_options
|
104
208
|
]
|
105
|
-
|
106
|
-
|
107
|
-
return translated_options[int(answer_code)]
|
108
|
-
|
109
|
-
def _simulate_answer(
|
110
|
-
self, human_readable: bool = True
|
111
|
-
) -> dict[str, Union[int, str]]:
|
112
|
-
"""Simulate a valid answer for debugging purposes."""
|
113
|
-
from edsl.utilities.utilities import random_string
|
114
|
-
|
115
|
-
if human_readable:
|
116
|
-
answer = random.choice(self.question_options)
|
209
|
+
if self._use_code:
|
210
|
+
return translated_options[int(answer_code)]
|
117
211
|
else:
|
118
|
-
|
119
|
-
|
120
|
-
"answer": answer,
|
121
|
-
"comment": random_string(),
|
122
|
-
}
|
212
|
+
# return translated_options[answer_code]
|
213
|
+
return answer_code
|
123
214
|
|
124
215
|
@property
|
125
216
|
def question_html_content(self) -> str:
|
@@ -153,33 +244,36 @@ class QuestionMultipleChoice(QuestionBase):
|
|
153
244
|
# Example
|
154
245
|
################
|
155
246
|
@classmethod
|
156
|
-
|
247
|
+
@inject_exception
|
248
|
+
def example(cls, include_comment=False, use_code=False) -> QuestionMultipleChoice:
|
157
249
|
"""Return an example instance."""
|
158
250
|
return cls(
|
159
251
|
question_text="How are you?",
|
160
252
|
question_options=["Good", "Great", "OK", "Bad"],
|
161
253
|
question_name="how_feeling",
|
254
|
+
include_comment=include_comment,
|
255
|
+
use_code=use_code,
|
162
256
|
)
|
163
257
|
|
164
258
|
|
165
|
-
def main():
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
259
|
+
# def main():
|
260
|
+
# """Create an example QuestionMultipleChoice and test its methods."""
|
261
|
+
# from edsl.questions.QuestionMultipleChoice import QuestionMultipleChoice
|
262
|
+
|
263
|
+
# q = QuestionMultipleChoice.example()
|
264
|
+
# q.question_text
|
265
|
+
# q.question_options
|
266
|
+
# q.question_name
|
267
|
+
# # validate an answer
|
268
|
+
# q._validate_answer({"answer": 0, "comment": "I like custard"})
|
269
|
+
# # translate answer code
|
270
|
+
# q._translate_answer_code_to_answer(0, {})
|
271
|
+
# # simulate answer
|
272
|
+
# q._simulate_answer()
|
273
|
+
# q._simulate_answer(human_readable=False)
|
274
|
+
# # serialization (inherits from Question)
|
275
|
+
# q.to_dict()
|
276
|
+
# assert q.from_dict(q.to_dict()) == q
|
183
277
|
|
184
278
|
|
185
279
|
if __name__ == "__main__":
|