edsl 0.1.33__py3-none-any.whl → 0.1.33.dev1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- edsl/Base.py +3 -9
- edsl/__init__.py +3 -8
- edsl/__version__.py +1 -1
- edsl/agents/Agent.py +8 -40
- edsl/agents/AgentList.py +0 -43
- edsl/agents/Invigilator.py +219 -135
- edsl/agents/InvigilatorBase.py +59 -148
- edsl/agents/{PromptConstructor.py → PromptConstructionMixin.py} +89 -138
- edsl/agents/__init__.py +0 -1
- edsl/config.py +56 -47
- edsl/coop/coop.py +7 -50
- edsl/data/Cache.py +1 -35
- edsl/data_transfer_models.py +38 -73
- edsl/enums.py +0 -4
- edsl/exceptions/language_models.py +1 -25
- edsl/exceptions/questions.py +5 -62
- edsl/exceptions/results.py +0 -4
- edsl/inference_services/AnthropicService.py +11 -13
- edsl/inference_services/AwsBedrock.py +17 -19
- edsl/inference_services/AzureAI.py +20 -37
- edsl/inference_services/GoogleService.py +12 -16
- edsl/inference_services/GroqService.py +0 -2
- edsl/inference_services/InferenceServiceABC.py +3 -58
- edsl/inference_services/OpenAIService.py +54 -48
- edsl/inference_services/models_available_cache.py +6 -0
- edsl/inference_services/registry.py +0 -6
- edsl/jobs/Answers.py +12 -10
- edsl/jobs/Jobs.py +21 -36
- edsl/jobs/buckets/BucketCollection.py +15 -24
- edsl/jobs/buckets/TokenBucket.py +14 -93
- edsl/jobs/interviews/Interview.py +78 -366
- edsl/jobs/interviews/InterviewExceptionEntry.py +19 -85
- edsl/jobs/interviews/InterviewTaskBuildingMixin.py +286 -0
- edsl/jobs/interviews/{InterviewExceptionCollection.py → interview_exception_tracking.py} +68 -14
- edsl/jobs/interviews/retry_management.py +37 -0
- edsl/jobs/runners/JobsRunnerAsyncio.py +175 -146
- edsl/jobs/runners/JobsRunnerStatusMixin.py +333 -0
- edsl/jobs/tasks/QuestionTaskCreator.py +23 -30
- edsl/jobs/tasks/TaskHistory.py +213 -148
- edsl/language_models/LanguageModel.py +156 -261
- edsl/language_models/ModelList.py +2 -2
- edsl/language_models/RegisterLanguageModelsMeta.py +29 -14
- edsl/language_models/registry.py +6 -23
- edsl/language_models/repair.py +19 -0
- edsl/prompts/Prompt.py +2 -52
- edsl/questions/AnswerValidatorMixin.py +26 -23
- edsl/questions/QuestionBase.py +249 -329
- edsl/questions/QuestionBudget.py +41 -99
- edsl/questions/QuestionCheckBox.py +35 -227
- edsl/questions/QuestionExtract.py +27 -98
- edsl/questions/QuestionFreeText.py +29 -52
- edsl/questions/QuestionFunctional.py +0 -7
- edsl/questions/QuestionList.py +22 -141
- edsl/questions/QuestionMultipleChoice.py +65 -159
- edsl/questions/QuestionNumerical.py +46 -88
- edsl/questions/QuestionRank.py +24 -182
- edsl/questions/RegisterQuestionsMeta.py +12 -31
- edsl/questions/__init__.py +4 -3
- edsl/questions/derived/QuestionLikertFive.py +5 -10
- edsl/questions/derived/QuestionLinearScale.py +2 -15
- edsl/questions/derived/QuestionTopK.py +1 -10
- edsl/questions/derived/QuestionYesNo.py +3 -24
- edsl/questions/descriptors.py +7 -43
- edsl/questions/question_registry.py +2 -6
- edsl/results/Dataset.py +0 -20
- edsl/results/DatasetExportMixin.py +48 -46
- edsl/results/Result.py +5 -32
- edsl/results/Results.py +46 -135
- edsl/results/ResultsDBMixin.py +3 -3
- edsl/scenarios/FileStore.py +10 -71
- edsl/scenarios/Scenario.py +25 -96
- edsl/scenarios/ScenarioImageMixin.py +2 -2
- edsl/scenarios/ScenarioList.py +39 -361
- edsl/scenarios/ScenarioListExportMixin.py +0 -9
- edsl/scenarios/ScenarioListPdfMixin.py +4 -150
- edsl/study/SnapShot.py +1 -8
- edsl/study/Study.py +0 -32
- edsl/surveys/Rule.py +1 -10
- edsl/surveys/RuleCollection.py +5 -21
- edsl/surveys/Survey.py +310 -636
- edsl/surveys/SurveyExportMixin.py +9 -71
- edsl/surveys/SurveyFlowVisualizationMixin.py +1 -2
- edsl/surveys/SurveyQualtricsImport.py +4 -75
- edsl/utilities/gcp_bucket/simple_example.py +9 -0
- edsl/utilities/utilities.py +1 -9
- {edsl-0.1.33.dist-info → edsl-0.1.33.dev1.dist-info}/METADATA +2 -5
- edsl-0.1.33.dev1.dist-info/RECORD +209 -0
- edsl/TemplateLoader.py +0 -24
- edsl/auto/AutoStudy.py +0 -117
- edsl/auto/StageBase.py +0 -230
- edsl/auto/StageGenerateSurvey.py +0 -178
- edsl/auto/StageLabelQuestions.py +0 -125
- edsl/auto/StagePersona.py +0 -61
- edsl/auto/StagePersonaDimensionValueRanges.py +0 -88
- edsl/auto/StagePersonaDimensionValues.py +0 -74
- edsl/auto/StagePersonaDimensions.py +0 -69
- edsl/auto/StageQuestions.py +0 -73
- edsl/auto/SurveyCreatorPipeline.py +0 -21
- edsl/auto/utilities.py +0 -224
- edsl/coop/PriceFetcher.py +0 -58
- edsl/inference_services/MistralAIService.py +0 -120
- edsl/inference_services/TestService.py +0 -80
- edsl/inference_services/TogetherAIService.py +0 -170
- edsl/jobs/FailedQuestion.py +0 -78
- edsl/jobs/runners/JobsRunnerStatus.py +0 -331
- edsl/language_models/fake_openai_call.py +0 -15
- edsl/language_models/fake_openai_service.py +0 -61
- edsl/language_models/utilities.py +0 -61
- edsl/questions/QuestionBaseGenMixin.py +0 -133
- edsl/questions/QuestionBasePromptsMixin.py +0 -266
- edsl/questions/Quick.py +0 -41
- edsl/questions/ResponseValidatorABC.py +0 -170
- edsl/questions/decorators.py +0 -21
- edsl/questions/prompt_templates/question_budget.jinja +0 -13
- edsl/questions/prompt_templates/question_checkbox.jinja +0 -32
- edsl/questions/prompt_templates/question_extract.jinja +0 -11
- edsl/questions/prompt_templates/question_free_text.jinja +0 -3
- edsl/questions/prompt_templates/question_linear_scale.jinja +0 -11
- edsl/questions/prompt_templates/question_list.jinja +0 -17
- edsl/questions/prompt_templates/question_multiple_choice.jinja +0 -33
- edsl/questions/prompt_templates/question_numerical.jinja +0 -37
- edsl/questions/templates/__init__.py +0 -0
- edsl/questions/templates/budget/__init__.py +0 -0
- edsl/questions/templates/budget/answering_instructions.jinja +0 -7
- edsl/questions/templates/budget/question_presentation.jinja +0 -7
- edsl/questions/templates/checkbox/__init__.py +0 -0
- edsl/questions/templates/checkbox/answering_instructions.jinja +0 -10
- edsl/questions/templates/checkbox/question_presentation.jinja +0 -22
- edsl/questions/templates/extract/__init__.py +0 -0
- edsl/questions/templates/extract/answering_instructions.jinja +0 -7
- edsl/questions/templates/extract/question_presentation.jinja +0 -1
- 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 +0 -1
- edsl/questions/templates/likert_five/__init__.py +0 -0
- edsl/questions/templates/likert_five/answering_instructions.jinja +0 -10
- edsl/questions/templates/likert_five/question_presentation.jinja +0 -12
- edsl/questions/templates/linear_scale/__init__.py +0 -0
- edsl/questions/templates/linear_scale/answering_instructions.jinja +0 -5
- edsl/questions/templates/linear_scale/question_presentation.jinja +0 -5
- edsl/questions/templates/list/__init__.py +0 -0
- edsl/questions/templates/list/answering_instructions.jinja +0 -4
- edsl/questions/templates/list/question_presentation.jinja +0 -5
- edsl/questions/templates/multiple_choice/__init__.py +0 -0
- edsl/questions/templates/multiple_choice/answering_instructions.jinja +0 -9
- edsl/questions/templates/multiple_choice/html.jinja +0 -0
- edsl/questions/templates/multiple_choice/question_presentation.jinja +0 -12
- edsl/questions/templates/numerical/__init__.py +0 -0
- edsl/questions/templates/numerical/answering_instructions.jinja +0 -8
- edsl/questions/templates/numerical/question_presentation.jinja +0 -7
- edsl/questions/templates/rank/__init__.py +0 -0
- edsl/questions/templates/rank/answering_instructions.jinja +0 -11
- edsl/questions/templates/rank/question_presentation.jinja +0 -15
- edsl/questions/templates/top_k/__init__.py +0 -0
- edsl/questions/templates/top_k/answering_instructions.jinja +0 -8
- edsl/questions/templates/top_k/question_presentation.jinja +0 -22
- edsl/questions/templates/yes_no/__init__.py +0 -0
- edsl/questions/templates/yes_no/answering_instructions.jinja +0 -6
- edsl/questions/templates/yes_no/question_presentation.jinja +0 -12
- edsl/results/DatasetTree.py +0 -145
- edsl/results/Selector.py +0 -118
- edsl/results/tree_explore.py +0 -115
- edsl/surveys/instructions/ChangeInstruction.py +0 -47
- edsl/surveys/instructions/Instruction.py +0 -34
- edsl/surveys/instructions/InstructionCollection.py +0 -77
- edsl/surveys/instructions/__init__.py +0 -0
- edsl/templates/error_reporting/base.html +0 -24
- edsl/templates/error_reporting/exceptions_by_model.html +0 -35
- edsl/templates/error_reporting/exceptions_by_question_name.html +0 -17
- edsl/templates/error_reporting/exceptions_by_type.html +0 -17
- edsl/templates/error_reporting/interview_details.html +0 -116
- edsl/templates/error_reporting/interviews.html +0 -10
- edsl/templates/error_reporting/overview.html +0 -5
- edsl/templates/error_reporting/performance_plot.html +0 -2
- edsl/templates/error_reporting/report.css +0 -74
- edsl/templates/error_reporting/report.html +0 -118
- edsl/templates/error_reporting/report.js +0 -25
- edsl-0.1.33.dist-info/RECORD +0 -295
- {edsl-0.1.33.dist-info → edsl-0.1.33.dev1.dist-info}/LICENSE +0 -0
- {edsl-0.1.33.dist-info → edsl-0.1.33.dev1.dist-info}/WHEEL +0 -0
@@ -1,62 +1,23 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
+
import textwrap
|
2
3
|
from typing import Any, Optional
|
3
4
|
from uuid import uuid4
|
4
|
-
|
5
|
-
from pydantic import field_validator
|
6
|
-
|
7
5
|
from edsl.questions.QuestionBase import QuestionBase
|
8
|
-
from edsl.questions.ResponseValidatorABC import ResponseValidatorABC
|
9
|
-
|
10
|
-
from edsl.exceptions import QuestionAnswerValidationError
|
11
|
-
from edsl.questions.decorators import inject_exception
|
12
|
-
|
13
|
-
from pydantic import BaseModel
|
14
|
-
from typing import Optional, Any, List
|
15
|
-
|
16
|
-
from edsl.exceptions import QuestionAnswerValidationError
|
17
|
-
|
18
|
-
|
19
|
-
class FreeTextResponse(BaseModel):
|
20
|
-
"""
|
21
|
-
Validator for free text response questions.
|
22
|
-
"""
|
23
|
-
|
24
|
-
answer: str
|
25
|
-
generated_tokens: Optional[str] = None
|
26
|
-
|
27
|
-
|
28
|
-
class FreeTextResponseValidator(ResponseValidatorABC):
|
29
|
-
required_params = []
|
30
|
-
valid_examples = [({"answer": "This is great"}, {})]
|
31
|
-
invalid_examples = [
|
32
|
-
(
|
33
|
-
{"answer": None},
|
34
|
-
{},
|
35
|
-
"Answer code must not be missing.",
|
36
|
-
),
|
37
|
-
]
|
38
|
-
|
39
|
-
def fix(self, response, verbose=False):
|
40
|
-
return {
|
41
|
-
"answer": str(response.get("generated_tokens")),
|
42
|
-
"generated_tokens": str(response.get("generated_tokens")),
|
43
|
-
}
|
44
6
|
|
45
7
|
|
46
8
|
class QuestionFreeText(QuestionBase):
|
47
9
|
"""This question prompts the agent to respond with free text."""
|
48
10
|
|
49
11
|
question_type = "free_text"
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
):
|
12
|
+
default_instructions = textwrap.dedent(
|
13
|
+
"""\
|
14
|
+
You are being asked the following question: {{question_text}}
|
15
|
+
Return a valid JSON formatted like this:
|
16
|
+
{"answer": "<put free text answer here>"}
|
17
|
+
"""
|
18
|
+
)
|
19
|
+
|
20
|
+
def __init__(self, question_name: str, question_text: str):
|
60
21
|
"""Instantiate a new QuestionFreeText.
|
61
22
|
|
62
23
|
:param question_name: The name of the question.
|
@@ -64,8 +25,25 @@ class QuestionFreeText(QuestionBase):
|
|
64
25
|
"""
|
65
26
|
self.question_name = question_name
|
66
27
|
self.question_text = question_text
|
67
|
-
|
68
|
-
|
28
|
+
|
29
|
+
################
|
30
|
+
# Answer methods
|
31
|
+
################
|
32
|
+
def _validate_answer(self, answer: Any) -> dict[str, str]:
|
33
|
+
"""Validate the answer."""
|
34
|
+
self._validate_answer_template_basic(answer)
|
35
|
+
self._validate_answer_key_value(answer, "answer", str)
|
36
|
+
return answer
|
37
|
+
|
38
|
+
def _translate_answer_code_to_answer(self, answer, scenario: "Scenario" = None):
|
39
|
+
"""Do nothing, because the answer is already in a human-readable format."""
|
40
|
+
return answer
|
41
|
+
|
42
|
+
def _simulate_answer(self, human_readable: bool = True) -> dict[str, str]:
|
43
|
+
"""Simulate a valid answer for debugging purposes."""
|
44
|
+
from edsl.utilities.utilities import random_string
|
45
|
+
|
46
|
+
return {"answer": random_string()}
|
69
47
|
|
70
48
|
@property
|
71
49
|
def question_html_content(self) -> str:
|
@@ -81,7 +59,6 @@ class QuestionFreeText(QuestionBase):
|
|
81
59
|
return question_html_content
|
82
60
|
|
83
61
|
@classmethod
|
84
|
-
@inject_exception
|
85
62
|
def example(cls, randomize: bool = False) -> QuestionFreeText:
|
86
63
|
"""Return an example instance of a free text question."""
|
87
64
|
addition = "" if not randomize else str(uuid4())
|
@@ -39,9 +39,6 @@ class QuestionFunctional(QuestionBase):
|
|
39
39
|
function_source_code = ""
|
40
40
|
function_name = ""
|
41
41
|
|
42
|
-
_response_model = None
|
43
|
-
response_validator_class = None
|
44
|
-
|
45
42
|
def __init__(
|
46
43
|
self,
|
47
44
|
question_name: str,
|
@@ -100,10 +97,6 @@ class QuestionFunctional(QuestionBase):
|
|
100
97
|
"""Required by Question, but not used by QuestionFunctional."""
|
101
98
|
raise NotImplementedError
|
102
99
|
|
103
|
-
@property
|
104
|
-
def question_html_content(self) -> str:
|
105
|
-
return "NA for QuestionFunctional"
|
106
|
-
|
107
100
|
@add_edsl_version
|
108
101
|
def to_dict(self):
|
109
102
|
return {
|
edsl/questions/QuestionList.py
CHANGED
@@ -4,123 +4,6 @@ import textwrap
|
|
4
4
|
from typing import Any, Optional, Union
|
5
5
|
from edsl.questions.QuestionBase import QuestionBase
|
6
6
|
from edsl.questions.descriptors import IntegerOrNoneDescriptor
|
7
|
-
from edsl.questions.decorators import inject_exception
|
8
|
-
|
9
|
-
from pydantic import field_validator, Field
|
10
|
-
from edsl.questions.ResponseValidatorABC import ResponseValidatorABC
|
11
|
-
from edsl.questions.ResponseValidatorABC import BaseResponse
|
12
|
-
|
13
|
-
from edsl.exceptions import QuestionAnswerValidationError
|
14
|
-
import textwrap
|
15
|
-
import json
|
16
|
-
|
17
|
-
from json_repair import repair_json
|
18
|
-
|
19
|
-
|
20
|
-
def convert_string(s):
|
21
|
-
"""Convert a string to a more appropriate type if possible.
|
22
|
-
|
23
|
-
>>> convert_string("3.14")
|
24
|
-
3.14
|
25
|
-
>>> convert_string("42")
|
26
|
-
42
|
27
|
-
>>> convert_string("hello")
|
28
|
-
'hello'
|
29
|
-
>>> convert_string('{"key": "value"}')
|
30
|
-
{'key': 'value'}
|
31
|
-
>>> convert_string("{'key': 'value'}")
|
32
|
-
{'key': 'value'}
|
33
|
-
"""
|
34
|
-
|
35
|
-
if not isinstance(s, str): # if it's not a string, return it as is
|
36
|
-
return s
|
37
|
-
|
38
|
-
# If the repair returns, continue on; otherwise, try to load it as JSON
|
39
|
-
if (repaired_json := repair_json(s)) == '""':
|
40
|
-
pass
|
41
|
-
else:
|
42
|
-
try:
|
43
|
-
return json.loads(repaired_json)
|
44
|
-
except json.JSONDecodeError:
|
45
|
-
pass
|
46
|
-
|
47
|
-
# Try to convert to float
|
48
|
-
try:
|
49
|
-
return float(s)
|
50
|
-
except ValueError:
|
51
|
-
pass
|
52
|
-
|
53
|
-
# Try to convert to int
|
54
|
-
try:
|
55
|
-
return int(s)
|
56
|
-
except ValueError:
|
57
|
-
pass
|
58
|
-
|
59
|
-
# If all conversions fail, return the original string
|
60
|
-
return s
|
61
|
-
|
62
|
-
|
63
|
-
def create_model(max_list_items: int, permissive):
|
64
|
-
from pydantic import BaseModel
|
65
|
-
|
66
|
-
if permissive or max_list_items is None:
|
67
|
-
|
68
|
-
class ListResponse(BaseModel):
|
69
|
-
answer: list[Any]
|
70
|
-
comment: Optional[str] = None
|
71
|
-
generated_tokens: Optional[str] = None
|
72
|
-
|
73
|
-
else:
|
74
|
-
|
75
|
-
class ListResponse(BaseModel):
|
76
|
-
"""
|
77
|
-
>>> nr = ListResponse(answer=["Apple", "Cherry"])
|
78
|
-
>>> nr.dict()
|
79
|
-
{'answer': ['Apple', 'Cherry'], 'comment': None, 'generated_tokens': None}
|
80
|
-
"""
|
81
|
-
|
82
|
-
answer: list[Any] = Field(..., min_items=0, max_items=max_list_items)
|
83
|
-
comment: Optional[str] = None
|
84
|
-
generated_tokens: Optional[str] = None
|
85
|
-
|
86
|
-
return ListResponse
|
87
|
-
|
88
|
-
|
89
|
-
class ListResponseValidator(ResponseValidatorABC):
|
90
|
-
required_params = ["max_list_items", "permissive"]
|
91
|
-
valid_examples = [({"answer": ["hello", "world"]}, {"max_list_items": 5})]
|
92
|
-
|
93
|
-
invalid_examples = [
|
94
|
-
(
|
95
|
-
{"answer": ["hello", "world", "this", "is", "a", "test"]},
|
96
|
-
{"max_list_items": 5},
|
97
|
-
"Too many items.",
|
98
|
-
),
|
99
|
-
]
|
100
|
-
|
101
|
-
def _check_constraints(self, response) -> None:
|
102
|
-
if (
|
103
|
-
self.max_list_items is not None
|
104
|
-
and len(response.answer) > self.max_list_items
|
105
|
-
):
|
106
|
-
raise QuestionAnswerValidationError("Too many items.")
|
107
|
-
|
108
|
-
def fix(self, response, verbose=False):
|
109
|
-
if verbose:
|
110
|
-
print(f"Fixing list response: {response}")
|
111
|
-
answer = str(response.get("answer") or response.get("generated_tokens", ""))
|
112
|
-
if len(answer.split(",")) > 0:
|
113
|
-
return (
|
114
|
-
{"answer": answer.split(",")} | {"comment": response.get("comment")}
|
115
|
-
if "comment" in response
|
116
|
-
else {}
|
117
|
-
)
|
118
|
-
|
119
|
-
def _post_process(self, edsl_answer_dict):
|
120
|
-
edsl_answer_dict["answer"] = [
|
121
|
-
convert_string(item) for item in edsl_answer_dict["answer"]
|
122
|
-
]
|
123
|
-
return edsl_answer_dict
|
124
7
|
|
125
8
|
|
126
9
|
class QuestionList(QuestionBase):
|
@@ -128,38 +11,43 @@ class QuestionList(QuestionBase):
|
|
128
11
|
|
129
12
|
question_type = "list"
|
130
13
|
max_list_items: int = IntegerOrNoneDescriptor()
|
131
|
-
_response_model = None
|
132
|
-
response_validator_class = ListResponseValidator
|
133
14
|
|
134
15
|
def __init__(
|
135
16
|
self,
|
136
17
|
question_name: str,
|
137
18
|
question_text: str,
|
138
19
|
max_list_items: Optional[int] = None,
|
139
|
-
include_comment: bool = True,
|
140
|
-
answering_instructions: Optional[str] = None,
|
141
|
-
question_presentation: Optional[str] = None,
|
142
|
-
permissive: bool = False,
|
143
20
|
):
|
144
21
|
"""Instantiate a new QuestionList.
|
145
22
|
|
146
23
|
:param question_name: The name of the question.
|
147
24
|
:param question_text: The text of the question.
|
148
25
|
:param max_list_items: The maximum number of items that can be in the answer list.
|
149
|
-
|
150
|
-
>>> QuestionList.example().self_check()
|
151
26
|
"""
|
152
27
|
self.question_name = question_name
|
153
28
|
self.question_text = question_text
|
154
29
|
self.max_list_items = max_list_items
|
155
|
-
self.permissive = permissive
|
156
30
|
|
157
|
-
|
158
|
-
|
159
|
-
|
31
|
+
################
|
32
|
+
# Answer methods
|
33
|
+
################
|
34
|
+
def _validate_answer(self, answer: Any) -> dict[str, Union[list[str], str]]:
|
35
|
+
"""Validate the answer."""
|
36
|
+
self._validate_answer_template_basic(answer)
|
37
|
+
self._validate_answer_key_value(answer, "answer", list)
|
38
|
+
self._validate_answer_list(answer)
|
39
|
+
return answer
|
40
|
+
|
41
|
+
def _translate_answer_code_to_answer(self, answer, scenario: "Scenario" = None):
|
42
|
+
"""There is no answer code."""
|
43
|
+
return answer
|
44
|
+
|
45
|
+
def _simulate_answer(self, human_readable: bool = True):
|
46
|
+
"""Simulate a valid answer for debugging purposes (what the validator expects)."""
|
47
|
+
num_items = random.randint(1, self.max_list_items or 2)
|
48
|
+
from edsl.utilities.utilities import random_string
|
160
49
|
|
161
|
-
|
162
|
-
return create_model(self.max_list_items, self.permissive)
|
50
|
+
return {"answer": [random_string() for _ in range(num_items)]}
|
163
51
|
|
164
52
|
@property
|
165
53
|
def question_html_content(self) -> str:
|
@@ -190,17 +78,12 @@ class QuestionList(QuestionBase):
|
|
190
78
|
# Helpful methods
|
191
79
|
################
|
192
80
|
@classmethod
|
193
|
-
|
194
|
-
def example(
|
195
|
-
cls, include_comment=True, max_list_items=None, permissive=False
|
196
|
-
) -> QuestionList:
|
81
|
+
def example(cls) -> QuestionList:
|
197
82
|
"""Return an example of a list question."""
|
198
83
|
return cls(
|
199
84
|
question_name="list_of_foods",
|
200
85
|
question_text="What are your favorite foods?",
|
201
|
-
|
202
|
-
max_list_items=max_list_items,
|
203
|
-
permissive=permissive,
|
86
|
+
max_list_items=5,
|
204
87
|
)
|
205
88
|
|
206
89
|
|
@@ -208,7 +91,7 @@ def main():
|
|
208
91
|
"""Create an example of a list question and demonstrate its functionality."""
|
209
92
|
from edsl.questions.QuestionList import QuestionList
|
210
93
|
|
211
|
-
q = QuestionList.example(
|
94
|
+
q = QuestionList.example()
|
212
95
|
q.question_text
|
213
96
|
q.question_name
|
214
97
|
q.max_list_items
|
@@ -224,8 +107,6 @@ def main():
|
|
224
107
|
q.to_dict()
|
225
108
|
assert q.from_dict(q.to_dict()) == q
|
226
109
|
|
227
|
-
|
228
|
-
if __name__ == "__main__":
|
229
110
|
import doctest
|
230
111
|
|
231
112
|
doctest.testmod(optionflags=doctest.ELLIPSIS)
|
@@ -1,114 +1,12 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
-
|
3
|
-
|
2
|
+
import time
|
3
|
+
from typing import Union
|
4
|
+
import random
|
5
|
+
from typing import Optional
|
4
6
|
from jinja2 import Template
|
5
|
-
from pydantic import BaseModel, Field
|
6
7
|
|
7
|
-
from edsl.scenarios.Scenario import Scenario
|
8
8
|
from edsl.questions.QuestionBase import QuestionBase
|
9
9
|
from edsl.questions.descriptors import QuestionOptionsDescriptor
|
10
|
-
from edsl.questions.decorators import inject_exception
|
11
|
-
from edsl.questions.ResponseValidatorABC import ResponseValidatorABC
|
12
|
-
|
13
|
-
|
14
|
-
def create_response_model(choices: List[str], permissive: bool = False):
|
15
|
-
"""
|
16
|
-
Create a ChoiceResponse model class with a predefined list of choices.
|
17
|
-
|
18
|
-
:param choices: A list of allowed values for the answer field.
|
19
|
-
:param permissive: If True, any value will be accepted as an answer.
|
20
|
-
:return: A new Pydantic model class.
|
21
|
-
"""
|
22
|
-
choice_tuple = tuple(choices)
|
23
|
-
|
24
|
-
if not permissive:
|
25
|
-
|
26
|
-
class ChoiceResponse(BaseModel):
|
27
|
-
answer: Literal[choice_tuple] = Field(description="Selected choice")
|
28
|
-
comment: Optional[str] = Field(None, description="Optional comment field")
|
29
|
-
generated_tokens: Optional[Any] = Field(
|
30
|
-
None, description="Generated tokens"
|
31
|
-
)
|
32
|
-
|
33
|
-
class Config:
|
34
|
-
@staticmethod
|
35
|
-
def json_schema_extra(schema: dict, model: BaseModel) -> None:
|
36
|
-
for prop in schema.get("properties", {}).values():
|
37
|
-
if prop.get("title") == "answer":
|
38
|
-
prop["enum"] = choices
|
39
|
-
|
40
|
-
else:
|
41
|
-
|
42
|
-
class ChoiceResponse(BaseModel):
|
43
|
-
answer: Any = Field(description="Selected choice (can be any value)")
|
44
|
-
comment: Optional[str] = Field(None, description="Optional comment field")
|
45
|
-
generated_tokens: Optional[Any] = Field(
|
46
|
-
None, description="Generated tokens"
|
47
|
-
)
|
48
|
-
|
49
|
-
class Config:
|
50
|
-
@staticmethod
|
51
|
-
def json_schema_extra(schema: dict, model: BaseModel) -> None:
|
52
|
-
for prop in schema.get("properties", {}).values():
|
53
|
-
if prop.get("title") == "answer":
|
54
|
-
prop["description"] += f". Suggested choices are: {choices}"
|
55
|
-
schema["title"] += " (Permissive)"
|
56
|
-
|
57
|
-
return ChoiceResponse
|
58
|
-
|
59
|
-
|
60
|
-
class MultipleChoiceResponseValidator(ResponseValidatorABC):
|
61
|
-
required_params = ["question_options", "use_code"]
|
62
|
-
|
63
|
-
def fix(self, response, verbose=False):
|
64
|
-
response_text = str(response.get("answer"))
|
65
|
-
if response_text is None:
|
66
|
-
response_text = response.get("generated_tokens", "")
|
67
|
-
|
68
|
-
if verbose:
|
69
|
-
print(f"Invalid generated tokens was: {response_text}")
|
70
|
-
|
71
|
-
matches = []
|
72
|
-
for idx, option in enumerate(self.question_options):
|
73
|
-
if verbose:
|
74
|
-
print("The options are: ", self.question_options)
|
75
|
-
if str(option) in response_text:
|
76
|
-
if verbose:
|
77
|
-
print("Match found with option ", option)
|
78
|
-
if option not in matches:
|
79
|
-
matches.append(option)
|
80
|
-
|
81
|
-
if verbose:
|
82
|
-
print("The matches are: ", matches)
|
83
|
-
if len(matches) == 1:
|
84
|
-
proposed_data = {
|
85
|
-
"answer": matches[0],
|
86
|
-
"generated_tokens": response.get("generated_tokens", None),
|
87
|
-
}
|
88
|
-
try:
|
89
|
-
self.response_model(**proposed_data)
|
90
|
-
return proposed_data
|
91
|
-
except Exception as e:
|
92
|
-
if verbose:
|
93
|
-
print(f"Proposed solution {proposed_data} is invalid. Error: {e}")
|
94
|
-
return response
|
95
|
-
|
96
|
-
valid_examples = [
|
97
|
-
({"answer": 1}, {"question_options": ["Good", "Great", "OK", "Bad"]})
|
98
|
-
]
|
99
|
-
|
100
|
-
invalid_examples = [
|
101
|
-
(
|
102
|
-
{"answer": -1},
|
103
|
-
{"question_options": ["Good", "Great", "OK", "Bad"]},
|
104
|
-
"Answer code must be a non-negative integer",
|
105
|
-
),
|
106
|
-
(
|
107
|
-
{"answer": None},
|
108
|
-
{"question_options": ["Good", "Great", "OK", "Bad"]},
|
109
|
-
"Answer code must not be missing.",
|
110
|
-
),
|
111
|
-
]
|
112
10
|
|
113
11
|
|
114
12
|
class QuestionMultipleChoice(QuestionBase):
|
@@ -123,53 +21,49 @@ class QuestionMultipleChoice(QuestionBase):
|
|
123
21
|
question_options: Union[
|
124
22
|
list[str], list[list], list[float], list[int]
|
125
23
|
] = QuestionOptionsDescriptor()
|
126
|
-
_response_model = None
|
127
|
-
response_validator_class = MultipleChoiceResponseValidator
|
128
24
|
|
129
25
|
def __init__(
|
130
26
|
self,
|
131
27
|
question_name: str,
|
132
28
|
question_text: str,
|
133
29
|
question_options: Union[list[str], list[list], list[float], list[int]],
|
134
|
-
include_comment: bool = True,
|
135
|
-
use_code: bool = False,
|
136
|
-
answering_instructions: Optional[str] = None,
|
137
|
-
question_presentation: Optional[str] = None,
|
138
|
-
permissive: bool = False,
|
139
30
|
):
|
140
31
|
"""Instantiate a new QuestionMultipleChoice.
|
141
32
|
|
142
33
|
:param question_name: The name of the question.
|
143
34
|
:param question_text: The text of the question.
|
144
35
|
:param question_options: The options the agent should select from.
|
145
|
-
:param include_comment: Whether to include a comment field.
|
146
|
-
:param use_code: Whether to use code for the options.
|
147
|
-
:param answering_instructions: Instructions for the question.
|
148
|
-
:param question_presentation: The presentation of the question.
|
149
|
-
:param permissive: Whether to force the answer to be one of the options.
|
150
|
-
|
151
36
|
"""
|
152
37
|
self.question_name = question_name
|
153
38
|
self.question_text = question_text
|
154
39
|
self.question_options = question_options
|
155
40
|
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
self.permissive = permissive
|
41
|
+
# @property
|
42
|
+
# def question_options(self) -> Union[list[str], list[list], list[float], list[int]]:
|
43
|
+
# """Return the question options."""
|
44
|
+
# return self._question_options
|
161
45
|
|
162
46
|
################
|
163
47
|
# Answer methods
|
164
48
|
################
|
49
|
+
def _validate_answer(
|
50
|
+
self, answer: dict[str, Union[str, int]]
|
51
|
+
) -> dict[str, Union[str, int]]:
|
52
|
+
"""Validate the answer.
|
165
53
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
54
|
+
>>> q = QuestionMultipleChoice.example()
|
55
|
+
>>> q._validate_answer({"answer": 0, "comment": "I like custard"})
|
56
|
+
{'answer': 0, 'comment': 'I like custard'}
|
57
|
+
|
58
|
+
>>> q = QuestionMultipleChoice(question_name="how_feeling", question_text="How are you?", question_options=["Good", "Great", "OK", "Bad"])
|
59
|
+
>>> q._validate_answer({"answer": -1, "comment": "I like custard"})
|
60
|
+
Traceback (most recent call last):
|
61
|
+
...
|
62
|
+
edsl.exceptions.questions.QuestionAnswerValidationError: Answer code must be a non-negative integer (got -1).
|
63
|
+
"""
|
64
|
+
self._validate_answer_template_basic(answer)
|
65
|
+
self._validate_answer_multiple_choice(answer)
|
66
|
+
return answer
|
173
67
|
|
174
68
|
def _translate_answer_code_to_answer(
|
175
69
|
self, answer_code: int, scenario: Optional["Scenario"] = None
|
@@ -180,14 +74,15 @@ class QuestionMultipleChoice(QuestionBase):
|
|
180
74
|
The question options might be templates, so they need to be rendered with the scenario.
|
181
75
|
|
182
76
|
>>> q = QuestionMultipleChoice.example()
|
183
|
-
>>> q._translate_answer_code_to_answer(
|
77
|
+
>>> q._translate_answer_code_to_answer(0, {})
|
184
78
|
'Good'
|
185
79
|
|
186
80
|
>>> q = QuestionMultipleChoice(question_name="how_feeling", question_text="How are you?", question_options=["{{emotion[0]}}", "emotion[1]"])
|
187
|
-
>>> q._translate_answer_code_to_answer(
|
81
|
+
>>> q._translate_answer_code_to_answer(0, {"emotion": ["Happy", "Sad"]})
|
188
82
|
'Happy'
|
189
83
|
|
190
84
|
"""
|
85
|
+
from edsl.scenarios.Scenario import Scenario
|
191
86
|
|
192
87
|
scenario = scenario or Scenario()
|
193
88
|
|
@@ -200,17 +95,31 @@ class QuestionMultipleChoice(QuestionBase):
|
|
200
95
|
question_option_key = list(meta.find_undeclared_variables(parsed_content))[
|
201
96
|
0
|
202
97
|
]
|
98
|
+
# breakpoint()
|
203
99
|
translated_options = scenario.get(question_option_key)
|
204
100
|
else:
|
205
101
|
translated_options = [
|
206
102
|
Template(str(option)).render(scenario)
|
207
103
|
for option in self.question_options
|
208
104
|
]
|
209
|
-
|
210
|
-
|
105
|
+
# print("Translated options:", translated_options)
|
106
|
+
# breakpoint()
|
107
|
+
return translated_options[int(answer_code)]
|
108
|
+
|
109
|
+
def _simulate_answer(
|
110
|
+
self, human_readable: bool = True
|
111
|
+
) -> dict[str, Union[int, str]]:
|
112
|
+
"""Simulate a valid answer for debugging purposes."""
|
113
|
+
from edsl.utilities.utilities import random_string
|
114
|
+
|
115
|
+
if human_readable:
|
116
|
+
answer = random.choice(self.question_options)
|
211
117
|
else:
|
212
|
-
|
213
|
-
|
118
|
+
answer = random.choice(range(len(self.question_options)))
|
119
|
+
return {
|
120
|
+
"answer": answer,
|
121
|
+
"comment": random_string(),
|
122
|
+
}
|
214
123
|
|
215
124
|
@property
|
216
125
|
def question_html_content(self) -> str:
|
@@ -244,36 +153,33 @@ class QuestionMultipleChoice(QuestionBase):
|
|
244
153
|
# Example
|
245
154
|
################
|
246
155
|
@classmethod
|
247
|
-
|
248
|
-
def example(cls, include_comment=False, use_code=False) -> QuestionMultipleChoice:
|
156
|
+
def example(cls) -> QuestionMultipleChoice:
|
249
157
|
"""Return an example instance."""
|
250
158
|
return cls(
|
251
159
|
question_text="How are you?",
|
252
160
|
question_options=["Good", "Great", "OK", "Bad"],
|
253
161
|
question_name="how_feeling",
|
254
|
-
include_comment=include_comment,
|
255
|
-
use_code=use_code,
|
256
162
|
)
|
257
163
|
|
258
164
|
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
#
|
268
|
-
|
269
|
-
#
|
270
|
-
|
271
|
-
#
|
272
|
-
|
273
|
-
|
274
|
-
#
|
275
|
-
|
276
|
-
|
165
|
+
def main():
|
166
|
+
"""Create an example QuestionMultipleChoice and test its methods."""
|
167
|
+
from edsl.questions.QuestionMultipleChoice import QuestionMultipleChoice
|
168
|
+
|
169
|
+
q = QuestionMultipleChoice.example()
|
170
|
+
q.question_text
|
171
|
+
q.question_options
|
172
|
+
q.question_name
|
173
|
+
# validate an answer
|
174
|
+
q._validate_answer({"answer": 0, "comment": "I like custard"})
|
175
|
+
# translate answer code
|
176
|
+
q._translate_answer_code_to_answer(0, {})
|
177
|
+
# simulate answer
|
178
|
+
q._simulate_answer()
|
179
|
+
q._simulate_answer(human_readable=False)
|
180
|
+
# serialization (inherits from Question)
|
181
|
+
q.to_dict()
|
182
|
+
assert q.from_dict(q.to_dict()) == q
|
277
183
|
|
278
184
|
|
279
185
|
if __name__ == "__main__":
|