edsl 0.1.38.dev4__py3-none-any.whl → 0.1.39__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 +197 -116
- edsl/__init__.py +15 -7
- edsl/__version__.py +1 -1
- edsl/agents/Agent.py +351 -147
- edsl/agents/AgentList.py +211 -73
- edsl/agents/Invigilator.py +101 -50
- edsl/agents/InvigilatorBase.py +62 -70
- edsl/agents/PromptConstructor.py +143 -225
- edsl/agents/QuestionInstructionPromptBuilder.py +128 -0
- edsl/agents/QuestionTemplateReplacementsBuilder.py +137 -0
- edsl/agents/__init__.py +0 -1
- edsl/agents/prompt_helpers.py +3 -3
- edsl/agents/question_option_processor.py +172 -0
- edsl/auto/AutoStudy.py +18 -5
- edsl/auto/StageBase.py +53 -40
- edsl/auto/StageQuestions.py +2 -1
- edsl/auto/utilities.py +0 -6
- edsl/config.py +22 -2
- edsl/conversation/car_buying.py +2 -1
- edsl/coop/CoopFunctionsMixin.py +15 -0
- edsl/coop/ExpectedParrotKeyHandler.py +125 -0
- edsl/coop/PriceFetcher.py +1 -1
- edsl/coop/coop.py +125 -47
- edsl/coop/utils.py +14 -14
- edsl/data/Cache.py +45 -27
- edsl/data/CacheEntry.py +12 -15
- edsl/data/CacheHandler.py +31 -12
- edsl/data/RemoteCacheSync.py +154 -46
- edsl/data/__init__.py +4 -3
- edsl/data_transfer_models.py +2 -1
- edsl/enums.py +27 -0
- edsl/exceptions/__init__.py +50 -50
- edsl/exceptions/agents.py +12 -0
- edsl/exceptions/inference_services.py +5 -0
- edsl/exceptions/questions.py +24 -6
- edsl/exceptions/scenarios.py +7 -0
- edsl/inference_services/AnthropicService.py +38 -19
- edsl/inference_services/AvailableModelCacheHandler.py +184 -0
- edsl/inference_services/AvailableModelFetcher.py +215 -0
- edsl/inference_services/AwsBedrock.py +0 -2
- edsl/inference_services/AzureAI.py +0 -2
- edsl/inference_services/GoogleService.py +7 -12
- edsl/inference_services/InferenceServiceABC.py +18 -85
- edsl/inference_services/InferenceServicesCollection.py +120 -79
- edsl/inference_services/MistralAIService.py +0 -3
- edsl/inference_services/OpenAIService.py +47 -35
- edsl/inference_services/PerplexityService.py +0 -3
- edsl/inference_services/ServiceAvailability.py +135 -0
- edsl/inference_services/TestService.py +11 -10
- edsl/inference_services/TogetherAIService.py +5 -3
- edsl/inference_services/data_structures.py +134 -0
- edsl/jobs/AnswerQuestionFunctionConstructor.py +223 -0
- edsl/jobs/Answers.py +1 -14
- edsl/jobs/FetchInvigilator.py +47 -0
- edsl/jobs/InterviewTaskManager.py +98 -0
- edsl/jobs/InterviewsConstructor.py +50 -0
- edsl/jobs/Jobs.py +356 -431
- edsl/jobs/JobsChecks.py +35 -10
- edsl/jobs/JobsComponentConstructor.py +189 -0
- edsl/jobs/JobsPrompts.py +6 -4
- edsl/jobs/JobsRemoteInferenceHandler.py +205 -133
- edsl/jobs/JobsRemoteInferenceLogger.py +239 -0
- edsl/jobs/RequestTokenEstimator.py +30 -0
- edsl/jobs/async_interview_runner.py +138 -0
- edsl/jobs/buckets/BucketCollection.py +44 -3
- edsl/jobs/buckets/TokenBucket.py +53 -21
- edsl/jobs/buckets/TokenBucketAPI.py +211 -0
- edsl/jobs/buckets/TokenBucketClient.py +191 -0
- edsl/jobs/check_survey_scenario_compatibility.py +85 -0
- edsl/jobs/data_structures.py +120 -0
- edsl/jobs/decorators.py +35 -0
- edsl/jobs/interviews/Interview.py +143 -408
- edsl/jobs/jobs_status_enums.py +9 -0
- edsl/jobs/loggers/HTMLTableJobLogger.py +304 -0
- edsl/jobs/results_exceptions_handler.py +98 -0
- edsl/jobs/runners/JobsRunnerAsyncio.py +88 -403
- edsl/jobs/runners/JobsRunnerStatus.py +133 -165
- edsl/jobs/tasks/QuestionTaskCreator.py +21 -19
- edsl/jobs/tasks/TaskHistory.py +38 -18
- edsl/jobs/tasks/task_status_enum.py +0 -2
- edsl/language_models/ComputeCost.py +63 -0
- edsl/language_models/LanguageModel.py +194 -236
- edsl/language_models/ModelList.py +28 -19
- edsl/language_models/PriceManager.py +127 -0
- edsl/language_models/RawResponseHandler.py +106 -0
- edsl/language_models/ServiceDataSources.py +0 -0
- edsl/language_models/__init__.py +1 -2
- edsl/language_models/key_management/KeyLookup.py +63 -0
- edsl/language_models/key_management/KeyLookupBuilder.py +273 -0
- edsl/language_models/key_management/KeyLookupCollection.py +38 -0
- edsl/language_models/key_management/__init__.py +0 -0
- edsl/language_models/key_management/models.py +131 -0
- edsl/language_models/model.py +256 -0
- edsl/language_models/repair.py +2 -2
- edsl/language_models/utilities.py +5 -4
- edsl/notebooks/Notebook.py +19 -14
- edsl/notebooks/NotebookToLaTeX.py +142 -0
- edsl/prompts/Prompt.py +29 -39
- edsl/questions/ExceptionExplainer.py +77 -0
- edsl/questions/HTMLQuestion.py +103 -0
- edsl/questions/QuestionBase.py +68 -214
- edsl/questions/QuestionBasePromptsMixin.py +7 -3
- edsl/questions/QuestionBudget.py +1 -1
- edsl/questions/QuestionCheckBox.py +3 -3
- edsl/questions/QuestionExtract.py +5 -7
- edsl/questions/QuestionFreeText.py +2 -3
- edsl/questions/QuestionList.py +10 -18
- edsl/questions/QuestionMatrix.py +265 -0
- edsl/questions/QuestionMultipleChoice.py +67 -23
- edsl/questions/QuestionNumerical.py +2 -4
- edsl/questions/QuestionRank.py +7 -17
- edsl/questions/SimpleAskMixin.py +4 -3
- edsl/questions/__init__.py +2 -1
- edsl/questions/{AnswerValidatorMixin.py → answer_validator_mixin.py} +47 -2
- edsl/questions/data_structures.py +20 -0
- edsl/questions/derived/QuestionLinearScale.py +6 -3
- edsl/questions/derived/QuestionTopK.py +1 -1
- edsl/questions/descriptors.py +17 -3
- edsl/questions/loop_processor.py +149 -0
- edsl/questions/{QuestionBaseGenMixin.py → question_base_gen_mixin.py} +57 -50
- edsl/questions/question_registry.py +1 -1
- edsl/questions/{ResponseValidatorABC.py → response_validator_abc.py} +40 -26
- edsl/questions/response_validator_factory.py +34 -0
- edsl/questions/templates/matrix/__init__.py +1 -0
- edsl/questions/templates/matrix/answering_instructions.jinja +5 -0
- edsl/questions/templates/matrix/question_presentation.jinja +20 -0
- edsl/results/CSSParameterizer.py +1 -1
- edsl/results/Dataset.py +170 -7
- edsl/results/DatasetExportMixin.py +168 -305
- edsl/results/DatasetTree.py +28 -8
- edsl/results/MarkdownToDocx.py +122 -0
- edsl/results/MarkdownToPDF.py +111 -0
- edsl/results/Result.py +298 -206
- edsl/results/Results.py +149 -131
- edsl/results/ResultsExportMixin.py +2 -0
- edsl/results/TableDisplay.py +98 -171
- edsl/results/TextEditor.py +50 -0
- edsl/results/__init__.py +1 -1
- edsl/results/file_exports.py +252 -0
- edsl/results/{Selector.py → results_selector.py} +23 -13
- edsl/results/smart_objects.py +96 -0
- edsl/results/table_data_class.py +12 -0
- edsl/results/table_renderers.py +118 -0
- edsl/scenarios/ConstructDownloadLink.py +109 -0
- edsl/scenarios/DocumentChunker.py +102 -0
- edsl/scenarios/DocxScenario.py +16 -0
- edsl/scenarios/FileStore.py +150 -239
- edsl/scenarios/PdfExtractor.py +40 -0
- edsl/scenarios/Scenario.py +90 -193
- edsl/scenarios/ScenarioHtmlMixin.py +4 -3
- edsl/scenarios/ScenarioList.py +415 -244
- edsl/scenarios/ScenarioListExportMixin.py +0 -7
- edsl/scenarios/ScenarioListPdfMixin.py +15 -37
- edsl/scenarios/__init__.py +1 -2
- edsl/scenarios/directory_scanner.py +96 -0
- edsl/scenarios/file_methods.py +85 -0
- edsl/scenarios/handlers/__init__.py +13 -0
- edsl/scenarios/handlers/csv.py +49 -0
- edsl/scenarios/handlers/docx.py +76 -0
- edsl/scenarios/handlers/html.py +37 -0
- edsl/scenarios/handlers/json.py +111 -0
- edsl/scenarios/handlers/latex.py +5 -0
- edsl/scenarios/handlers/md.py +51 -0
- edsl/scenarios/handlers/pdf.py +68 -0
- edsl/scenarios/handlers/png.py +39 -0
- edsl/scenarios/handlers/pptx.py +105 -0
- edsl/scenarios/handlers/py.py +294 -0
- edsl/scenarios/handlers/sql.py +313 -0
- edsl/scenarios/handlers/sqlite.py +149 -0
- edsl/scenarios/handlers/txt.py +33 -0
- edsl/scenarios/{ScenarioJoin.py → scenario_join.py} +10 -6
- edsl/scenarios/scenario_selector.py +156 -0
- edsl/study/ObjectEntry.py +1 -1
- edsl/study/SnapShot.py +1 -1
- edsl/study/Study.py +5 -12
- edsl/surveys/ConstructDAG.py +92 -0
- edsl/surveys/EditSurvey.py +221 -0
- edsl/surveys/InstructionHandler.py +100 -0
- edsl/surveys/MemoryManagement.py +72 -0
- edsl/surveys/Rule.py +5 -4
- edsl/surveys/RuleCollection.py +25 -27
- edsl/surveys/RuleManager.py +172 -0
- edsl/surveys/Simulator.py +75 -0
- edsl/surveys/Survey.py +270 -791
- edsl/surveys/SurveyCSS.py +20 -8
- edsl/surveys/{SurveyFlowVisualizationMixin.py → SurveyFlowVisualization.py} +11 -9
- edsl/surveys/SurveyToApp.py +141 -0
- edsl/surveys/__init__.py +4 -2
- edsl/surveys/descriptors.py +6 -2
- edsl/surveys/instructions/ChangeInstruction.py +1 -2
- edsl/surveys/instructions/Instruction.py +4 -13
- edsl/surveys/instructions/InstructionCollection.py +11 -6
- edsl/templates/error_reporting/interview_details.html +1 -1
- edsl/templates/error_reporting/report.html +1 -1
- edsl/tools/plotting.py +1 -1
- edsl/utilities/PrettyList.py +56 -0
- edsl/utilities/is_notebook.py +18 -0
- edsl/utilities/is_valid_variable_name.py +11 -0
- edsl/utilities/remove_edsl_version.py +24 -0
- edsl/utilities/utilities.py +35 -23
- {edsl-0.1.38.dev4.dist-info → edsl-0.1.39.dist-info}/METADATA +12 -10
- edsl-0.1.39.dist-info/RECORD +358 -0
- {edsl-0.1.38.dev4.dist-info → edsl-0.1.39.dist-info}/WHEEL +1 -1
- edsl/language_models/KeyLookup.py +0 -30
- edsl/language_models/registry.py +0 -190
- edsl/language_models/unused/ReplicateBase.py +0 -83
- edsl/results/ResultsDBMixin.py +0 -238
- edsl-0.1.38.dev4.dist-info/RECORD +0 -277
- /edsl/questions/{RegisterQuestionsMeta.py → register_questions_meta.py} +0 -0
- /edsl/results/{ResultsFetchMixin.py → results_fetch_mixin.py} +0 -0
- /edsl/results/{ResultsToolsMixin.py → results_tools_mixin.py} +0 -0
- {edsl-0.1.38.dev4.dist-info → edsl-0.1.39.dist-info}/LICENSE +0 -0
@@ -0,0 +1,265 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
from typing import Union, Optional, Dict, List, Any
|
3
|
+
|
4
|
+
from pydantic import BaseModel, Field, field_validator
|
5
|
+
from jinja2 import Template
|
6
|
+
import random
|
7
|
+
from edsl.questions.QuestionBase import QuestionBase
|
8
|
+
from edsl.questions.descriptors import (
|
9
|
+
QuestionOptionsDescriptor,
|
10
|
+
OptionLabelDescriptor,
|
11
|
+
QuestionTextDescriptor,
|
12
|
+
)
|
13
|
+
from edsl.questions.response_validator_abc import ResponseValidatorABC
|
14
|
+
from edsl.questions.decorators import inject_exception
|
15
|
+
from edsl.exceptions.questions import (
|
16
|
+
QuestionAnswerValidationError,
|
17
|
+
QuestionCreationValidationError,
|
18
|
+
)
|
19
|
+
|
20
|
+
|
21
|
+
def create_matrix_response(
|
22
|
+
question_items: List[str],
|
23
|
+
question_options: List[Union[int, str, float]],
|
24
|
+
permissive: bool = False,
|
25
|
+
):
|
26
|
+
"""Create a response model for matrix questions.
|
27
|
+
|
28
|
+
The response model validates that:
|
29
|
+
1. All question items are answered
|
30
|
+
2. Each answer is from the allowed options
|
31
|
+
"""
|
32
|
+
|
33
|
+
if permissive:
|
34
|
+
|
35
|
+
class MatrixResponse(BaseModel):
|
36
|
+
answer: Dict[str, Any]
|
37
|
+
comment: Optional[str] = None
|
38
|
+
generated_tokens: Optional[Any] = None
|
39
|
+
|
40
|
+
else:
|
41
|
+
|
42
|
+
class MatrixResponse(BaseModel):
|
43
|
+
answer: Dict[str, Union[int, str, float]] = Field(
|
44
|
+
..., description="Mapping of items to selected options"
|
45
|
+
)
|
46
|
+
comment: Optional[str] = None
|
47
|
+
generated_tokens: Optional[Any] = None
|
48
|
+
|
49
|
+
@field_validator("answer")
|
50
|
+
def validate_answer(cls, v, values, **kwargs):
|
51
|
+
# Check that all items have responses
|
52
|
+
if not all(item in v for item in question_items):
|
53
|
+
missing = set(question_items) - set(v.keys())
|
54
|
+
raise ValueError(f"Missing responses for items: {missing}")
|
55
|
+
|
56
|
+
# Check that all responses are valid options
|
57
|
+
if not all(answer in question_options for answer in v.values()):
|
58
|
+
invalid = [ans for ans in v.values() if ans not in question_options]
|
59
|
+
raise ValueError(f"Invalid options selected: {invalid}")
|
60
|
+
return v
|
61
|
+
|
62
|
+
return MatrixResponse
|
63
|
+
|
64
|
+
|
65
|
+
class MatrixResponseValidator(ResponseValidatorABC):
|
66
|
+
required_params = ["question_items", "question_options", "permissive"]
|
67
|
+
|
68
|
+
valid_examples = [
|
69
|
+
(
|
70
|
+
{"answer": {"Item1": 1, "Item2": 2}},
|
71
|
+
{
|
72
|
+
"question_items": ["Item1", "Item2"],
|
73
|
+
"question_options": [1, 2, 3],
|
74
|
+
},
|
75
|
+
)
|
76
|
+
]
|
77
|
+
|
78
|
+
invalid_examples = [
|
79
|
+
(
|
80
|
+
{"answer": {"Item1": 1}},
|
81
|
+
{
|
82
|
+
"question_items": ["Item1", "Item2"],
|
83
|
+
"question_options": [1, 2, 3],
|
84
|
+
},
|
85
|
+
"Missing responses for some items",
|
86
|
+
),
|
87
|
+
(
|
88
|
+
{"answer": {"Item1": 4, "Item2": 5}},
|
89
|
+
{
|
90
|
+
"question_items": ["Item1", "Item2"],
|
91
|
+
"question_options": [1, 2, 3],
|
92
|
+
},
|
93
|
+
"Invalid options selected",
|
94
|
+
),
|
95
|
+
]
|
96
|
+
|
97
|
+
def fix(self, response, verbose=False):
|
98
|
+
if verbose:
|
99
|
+
print(f"Fixing matrix response: {response}")
|
100
|
+
|
101
|
+
# If we have generated tokens, try to parse them
|
102
|
+
if "generated_tokens" in response:
|
103
|
+
try:
|
104
|
+
import json
|
105
|
+
|
106
|
+
fixed = json.loads(response["generated_tokens"])
|
107
|
+
if isinstance(fixed, dict):
|
108
|
+
# Map numeric keys to question items
|
109
|
+
mapped_answer = {}
|
110
|
+
for idx, item in enumerate(self.question_items):
|
111
|
+
if str(idx) in fixed:
|
112
|
+
mapped_answer[item] = fixed[str(idx)]
|
113
|
+
if (
|
114
|
+
mapped_answer
|
115
|
+
): # Only return if we successfully mapped some answers
|
116
|
+
return {"answer": mapped_answer}
|
117
|
+
except:
|
118
|
+
pass
|
119
|
+
|
120
|
+
# If answer uses numeric keys, map them to question items
|
121
|
+
if "answer" in response and isinstance(response["answer"], dict):
|
122
|
+
if all(str(key).isdigit() for key in response["answer"].keys()):
|
123
|
+
mapped_answer = {}
|
124
|
+
for idx, item in enumerate(self.question_items):
|
125
|
+
if str(idx) in response["answer"]:
|
126
|
+
mapped_answer[item] = response["answer"][str(idx)]
|
127
|
+
if mapped_answer: # Only update if we successfully mapped some answers
|
128
|
+
response["answer"] = mapped_answer
|
129
|
+
|
130
|
+
return response
|
131
|
+
|
132
|
+
|
133
|
+
class QuestionMatrix(QuestionBase):
|
134
|
+
"""A question that presents a matrix/grid where multiple items are rated using the same scale."""
|
135
|
+
|
136
|
+
question_type = "matrix"
|
137
|
+
question_text: str = QuestionTextDescriptor()
|
138
|
+
question_items: List[str] = QuestionOptionsDescriptor()
|
139
|
+
question_options: List[Union[int, str, float]] = QuestionOptionsDescriptor()
|
140
|
+
option_labels: Optional[Dict[Union[int, str, float], str]] = OptionLabelDescriptor()
|
141
|
+
|
142
|
+
_response_model = None
|
143
|
+
response_validator_class = MatrixResponseValidator
|
144
|
+
|
145
|
+
def __init__(
|
146
|
+
self,
|
147
|
+
question_name: str,
|
148
|
+
question_text: str,
|
149
|
+
question_items: List[str],
|
150
|
+
question_options: List[Union[int, str, float]],
|
151
|
+
option_labels: Optional[Dict[Union[int, str, float], str]] = None,
|
152
|
+
include_comment: bool = True,
|
153
|
+
answering_instructions: Optional[str] = None,
|
154
|
+
question_presentation: Optional[str] = None,
|
155
|
+
permissive: bool = False,
|
156
|
+
):
|
157
|
+
"""Initialize a matrix question.
|
158
|
+
|
159
|
+
Args:
|
160
|
+
question_name: The name of the question
|
161
|
+
question_text: The text of the question
|
162
|
+
question_items: List of items to be rated
|
163
|
+
question_options: List of rating options
|
164
|
+
option_labels: Optional mapping of options to their labels
|
165
|
+
include_comment: Whether to include a comment field
|
166
|
+
answering_instructions: Optional custom instructions
|
167
|
+
question_presentation: Optional custom presentation
|
168
|
+
permissive: Whether to strictly validate responses
|
169
|
+
"""
|
170
|
+
self.question_name = question_name
|
171
|
+
|
172
|
+
try:
|
173
|
+
self.question_text = question_text
|
174
|
+
except Exception as e:
|
175
|
+
raise QuestionCreationValidationError(
|
176
|
+
"question_text cannot be empty or too short!"
|
177
|
+
) from e
|
178
|
+
|
179
|
+
self.question_items = question_items
|
180
|
+
self.question_options = question_options
|
181
|
+
self.option_labels = option_labels or {}
|
182
|
+
|
183
|
+
self.include_comment = include_comment
|
184
|
+
self.answering_instructions = answering_instructions
|
185
|
+
self.question_presentation = question_presentation
|
186
|
+
self.permissive = permissive
|
187
|
+
|
188
|
+
def create_response_model(self):
|
189
|
+
return create_matrix_response(
|
190
|
+
self.question_items, self.question_options, self.permissive
|
191
|
+
)
|
192
|
+
|
193
|
+
@property
|
194
|
+
def question_html_content(self) -> str:
|
195
|
+
"""Generate HTML representation of the matrix question."""
|
196
|
+
template = Template(
|
197
|
+
"""
|
198
|
+
<table class="matrix-question">
|
199
|
+
<tr>
|
200
|
+
<th></th>
|
201
|
+
{% for option in question_options %}
|
202
|
+
<th>
|
203
|
+
{{ option }}
|
204
|
+
{% if option in option_labels %}
|
205
|
+
<br>
|
206
|
+
<small>{{ option_labels[option] }}</small>
|
207
|
+
{% endif %}
|
208
|
+
</th>
|
209
|
+
{% endfor %}
|
210
|
+
</tr>
|
211
|
+
{% for item in question_items %}
|
212
|
+
<tr>
|
213
|
+
<td>{{ item }}</td>
|
214
|
+
{% for option in question_options %}
|
215
|
+
<td>
|
216
|
+
<input type="radio"
|
217
|
+
name="{{ question_name }}_{{ item }}"
|
218
|
+
value="{{ option }}"
|
219
|
+
id="{{ question_name }}_{{ item }}_{{ option }}">
|
220
|
+
</td>
|
221
|
+
{% endfor %}
|
222
|
+
</tr>
|
223
|
+
{% endfor %}
|
224
|
+
</table>
|
225
|
+
"""
|
226
|
+
)
|
227
|
+
|
228
|
+
return template.render(
|
229
|
+
question_name=self.question_name,
|
230
|
+
question_items=self.question_items,
|
231
|
+
question_options=self.question_options,
|
232
|
+
option_labels=self.option_labels,
|
233
|
+
)
|
234
|
+
|
235
|
+
@classmethod
|
236
|
+
@inject_exception
|
237
|
+
def example(cls) -> QuestionMatrix:
|
238
|
+
"""Return an example matrix question."""
|
239
|
+
return cls(
|
240
|
+
question_name="child_happiness",
|
241
|
+
question_text="How happy would you be with different numbers of children?",
|
242
|
+
question_items=[
|
243
|
+
"No children",
|
244
|
+
"1 child",
|
245
|
+
"2 children",
|
246
|
+
"3 or more children",
|
247
|
+
],
|
248
|
+
question_options=[1, 2, 3, 4, 5],
|
249
|
+
option_labels={1: "Very sad", 3: "Neutral", 5: "Extremely happy"},
|
250
|
+
)
|
251
|
+
|
252
|
+
def _simulate_answer(self) -> dict:
|
253
|
+
"""Simulate a random valid answer."""
|
254
|
+
return {
|
255
|
+
"answer": {
|
256
|
+
item: random.choice(self.question_options)
|
257
|
+
for item in self.question_items
|
258
|
+
}
|
259
|
+
}
|
260
|
+
|
261
|
+
|
262
|
+
if __name__ == "__main__":
|
263
|
+
import doctest
|
264
|
+
|
265
|
+
doctest.testmod(optionflags=doctest.ELLIPSIS)
|
@@ -8,7 +8,7 @@ from edsl.scenarios.Scenario import Scenario
|
|
8
8
|
from edsl.questions.QuestionBase import QuestionBase
|
9
9
|
from edsl.questions.descriptors import QuestionOptionsDescriptor
|
10
10
|
from edsl.questions.decorators import inject_exception
|
11
|
-
from edsl.questions.
|
11
|
+
from edsl.questions.response_validator_abc import ResponseValidatorABC
|
12
12
|
|
13
13
|
|
14
14
|
def create_response_model(choices: List[str], permissive: bool = False):
|
@@ -120,9 +120,9 @@ class QuestionMultipleChoice(QuestionBase):
|
|
120
120
|
|
121
121
|
question_type = "multiple_choice"
|
122
122
|
purpose = "When options are known and limited"
|
123
|
-
question_options: Union[
|
124
|
-
|
125
|
-
|
123
|
+
question_options: Union[list[str], list[list], list[float], list[int]] = (
|
124
|
+
QuestionOptionsDescriptor()
|
125
|
+
)
|
126
126
|
_response_model = None
|
127
127
|
response_validator_class = MultipleChoiceResponseValidator
|
128
128
|
|
@@ -175,8 +175,54 @@ class QuestionMultipleChoice(QuestionBase):
|
|
175
175
|
else:
|
176
176
|
return create_response_model(self.question_options, self.permissive)
|
177
177
|
|
178
|
+
@staticmethod
|
179
|
+
def _translate_question_options(
|
180
|
+
question_options, substitution_dict: dict
|
181
|
+
) -> list[str]:
|
182
|
+
|
183
|
+
if isinstance(question_options, str):
|
184
|
+
# If dynamic options are provided like {{ options }}, render them with the scenario
|
185
|
+
# We can check if it's in the Scenario.
|
186
|
+
from jinja2 import Environment, meta
|
187
|
+
|
188
|
+
env = Environment()
|
189
|
+
parsed_content = env.parse(question_options)
|
190
|
+
template_variables = list(meta.find_undeclared_variables(parsed_content))
|
191
|
+
# print("The template variables are: ", template_variables)
|
192
|
+
question_option_key = template_variables[0]
|
193
|
+
# We need to deal with possibility it's actually an answer to a question.
|
194
|
+
potential_replacement = substitution_dict.get(question_option_key, None)
|
195
|
+
|
196
|
+
if isinstance(potential_replacement, list):
|
197
|
+
# translated_options = potential_replacement
|
198
|
+
return potential_replacement
|
199
|
+
|
200
|
+
if isinstance(potential_replacement, QuestionBase):
|
201
|
+
if hasattr(potential_replacement, "answer") and isinstance(
|
202
|
+
potential_replacement.answer, list
|
203
|
+
):
|
204
|
+
return potential_replacement.answer
|
205
|
+
# translated_options = potential_replacement.answer
|
206
|
+
|
207
|
+
# if not isinstance(potential_replacement, list):
|
208
|
+
# translated_options = potential_replacement
|
209
|
+
|
210
|
+
if potential_replacement is None:
|
211
|
+
# Nope - maybe it's in the substition dict?
|
212
|
+
raise ValueError(
|
213
|
+
f"Could not find the key '{question_option_key}' in the scenario."
|
214
|
+
f"The substition dict was: '{substitution_dict}.'"
|
215
|
+
f"The question options were: '{question_options}'."
|
216
|
+
)
|
217
|
+
else:
|
218
|
+
translated_options = [
|
219
|
+
Template(str(option)).render(substitution_dict)
|
220
|
+
for option in question_options
|
221
|
+
]
|
222
|
+
return translated_options
|
223
|
+
|
178
224
|
def _translate_answer_code_to_answer(
|
179
|
-
self, answer_code: int,
|
225
|
+
self, answer_code: int, replacements_dict: Optional[dict] = None
|
180
226
|
):
|
181
227
|
"""Translate the answer code to the actual answer.
|
182
228
|
|
@@ -192,26 +238,24 @@ class QuestionMultipleChoice(QuestionBase):
|
|
192
238
|
'Happy'
|
193
239
|
|
194
240
|
"""
|
241
|
+
if replacements_dict is None:
|
242
|
+
replacements_dict = {}
|
243
|
+
translated_options = self._translate_question_options(
|
244
|
+
self.question_options, replacements_dict
|
245
|
+
)
|
195
246
|
|
196
|
-
scenario = scenario or Scenario()
|
197
|
-
|
198
|
-
if isinstance(self.question_options, str):
|
199
|
-
# If dynamic options are provided like {{ options }}, render them with the scenario
|
200
|
-
from jinja2 import Environment, meta
|
201
|
-
|
202
|
-
env = Environment()
|
203
|
-
parsed_content = env.parse(self.question_options)
|
204
|
-
question_option_key = list(meta.find_undeclared_variables(parsed_content))[
|
205
|
-
0
|
206
|
-
]
|
207
|
-
translated_options = scenario.get(question_option_key)
|
208
|
-
else:
|
209
|
-
translated_options = [
|
210
|
-
Template(str(option)).render(scenario)
|
211
|
-
for option in self.question_options
|
212
|
-
]
|
213
247
|
if self._use_code:
|
214
|
-
|
248
|
+
try:
|
249
|
+
return translated_options[int(answer_code)]
|
250
|
+
except IndexError:
|
251
|
+
raise ValueError(
|
252
|
+
f"Answer code is out of range. The answer code index was: {int(answer_code)}. The options were: {translated_options}."
|
253
|
+
)
|
254
|
+
except TypeError:
|
255
|
+
raise ValueError(
|
256
|
+
f"The answer code was: '{answer_code}.'",
|
257
|
+
f"The options were: '{translated_options}'.",
|
258
|
+
)
|
215
259
|
else:
|
216
260
|
# return translated_options[answer_code]
|
217
261
|
return answer_code
|
@@ -1,17 +1,15 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
# from decimal import Decimal
|
4
3
|
from random import uniform
|
5
4
|
from typing import Any, Optional, Union, Literal
|
6
5
|
|
7
6
|
from pydantic import BaseModel, Field, field_validator
|
8
7
|
|
9
|
-
from edsl.exceptions import QuestionAnswerValidationError
|
8
|
+
from edsl.exceptions.questions import QuestionAnswerValidationError
|
10
9
|
from edsl.questions.QuestionBase import QuestionBase
|
11
10
|
from edsl.questions.descriptors import NumericalOrNoneDescriptor
|
12
11
|
from edsl.questions.decorators import inject_exception
|
13
|
-
from edsl.questions.
|
14
|
-
from edsl.exceptions.questions import QuestionAnswerValidationError
|
12
|
+
from edsl.questions.response_validator_abc import ResponseValidatorABC
|
15
13
|
|
16
14
|
|
17
15
|
def create_numeric_response(
|
edsl/questions/QuestionRank.py
CHANGED
@@ -1,25 +1,14 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
-
import
|
3
|
-
|
4
|
-
from
|
5
|
-
from typing import Any, Optional, Union
|
6
|
-
from edsl.questions.QuestionBase import QuestionBase
|
7
|
-
from edsl.exceptions import QuestionAnswerValidationError
|
2
|
+
from typing import Optional, Any, List, Annotated, Literal
|
3
|
+
|
4
|
+
from pydantic import BaseModel, Field
|
8
5
|
|
6
|
+
from edsl.questions.QuestionBase import QuestionBase
|
9
7
|
from edsl.questions.descriptors import (
|
10
8
|
QuestionOptionsDescriptor,
|
11
9
|
NumSelectionsDescriptor,
|
12
10
|
)
|
13
|
-
|
14
|
-
from edsl.prompts import Prompt
|
15
|
-
|
16
|
-
from pydantic import field_validator
|
17
|
-
from edsl.questions.ResponseValidatorABC import ResponseValidatorABC
|
18
|
-
from edsl.questions.ResponseValidatorABC import BaseResponse
|
19
|
-
from edsl.exceptions import QuestionAnswerValidationError
|
20
|
-
|
21
|
-
from pydantic import BaseModel, Field, create_model
|
22
|
-
from typing import Optional, Any, List, Annotated, Literal
|
11
|
+
from edsl.questions.response_validator_abc import ResponseValidatorABC
|
23
12
|
|
24
13
|
|
25
14
|
def create_response_model(
|
@@ -201,7 +190,8 @@ class QuestionRank(QuestionBase):
|
|
201
190
|
self, answer_codes, scenario: Scenario = None
|
202
191
|
) -> list[str]:
|
203
192
|
"""Translate the answer code to the actual answer."""
|
204
|
-
from edsl.scenarios import Scenario
|
193
|
+
from edsl.scenarios.Scenario import Scenario
|
194
|
+
from jinja2 import Template
|
205
195
|
|
206
196
|
scenario = scenario or Scenario()
|
207
197
|
translated_options = [
|
edsl/questions/SimpleAskMixin.py
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
from rich.table import Table
|
2
|
-
from rich.console import Console
|
3
1
|
import math
|
4
2
|
|
5
3
|
|
@@ -10,6 +8,9 @@ def logprob_to_prob(logprob):
|
|
10
8
|
|
11
9
|
|
12
10
|
def format_output(data):
|
11
|
+
from rich.table import Table
|
12
|
+
from rich.console import Console
|
13
|
+
|
13
14
|
content = data["choices"][0]["logprobs"]["content"]
|
14
15
|
table = Table(show_header=True, header_style="bold magenta")
|
15
16
|
|
@@ -65,7 +66,7 @@ class SimpleAskMixin:
|
|
65
66
|
system_prompt="You are a helpful agent pretending to be a human. Do not break character",
|
66
67
|
top_logprobs=4,
|
67
68
|
):
|
68
|
-
from edsl import Model
|
69
|
+
from edsl.language_models.model import Model
|
69
70
|
|
70
71
|
if model is None:
|
71
72
|
model = Model()
|
edsl/questions/__init__.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Schemas
|
2
2
|
from edsl.questions.settings import Settings
|
3
|
-
from edsl.questions.
|
3
|
+
from edsl.questions.register_questions_meta import RegisterQuestionsMeta
|
4
4
|
|
5
5
|
# Base Class
|
6
6
|
from edsl.questions.QuestionBase import QuestionBase
|
@@ -11,6 +11,7 @@ from edsl.questions.QuestionExtract import QuestionExtract
|
|
11
11
|
from edsl.questions.QuestionFreeText import QuestionFreeText
|
12
12
|
from edsl.questions.QuestionFunctional import QuestionFunctional
|
13
13
|
from edsl.questions.QuestionList import QuestionList
|
14
|
+
from edsl.questions.QuestionMatrix import QuestionMatrix
|
14
15
|
from edsl.questions.QuestionMultipleChoice import QuestionMultipleChoice
|
15
16
|
from edsl.questions.QuestionNumerical import QuestionNumerical
|
16
17
|
from edsl.questions.QuestionBudget import QuestionBudget
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
import re
|
4
4
|
from typing import Any, Type, Union
|
5
|
-
from edsl.exceptions import (
|
5
|
+
from edsl.exceptions.questions import (
|
6
6
|
QuestionAnswerValidationError,
|
7
7
|
)
|
8
8
|
|
@@ -213,7 +213,12 @@ class AnswerValidatorMixin:
|
|
213
213
|
- is not less than `min_value`
|
214
214
|
- is not greater than `max_value`
|
215
215
|
"""
|
216
|
-
|
216
|
+
try:
|
217
|
+
value = float(answer.get("answer"))
|
218
|
+
except ValueError:
|
219
|
+
raise QuestionAnswerValidationError(
|
220
|
+
f"Answer must real number or convertible to a real number (got {answer.get('answer')})."
|
221
|
+
)
|
217
222
|
if self.min_value is not None and value < self.min_value:
|
218
223
|
raise QuestionAnswerValidationError(
|
219
224
|
f"Value {value} is less than {self.min_value}"
|
@@ -279,6 +284,46 @@ class AnswerValidatorMixin:
|
|
279
284
|
f"Rank answer {value}, but exactly {self.num_selections} selections required."
|
280
285
|
)
|
281
286
|
|
287
|
+
def _validate_answer_matrix(self, answer: dict[str, Any]) -> None:
|
288
|
+
"""Validate QuestionMatrix-specific answer.
|
289
|
+
|
290
|
+
Check that answer["answer"]:
|
291
|
+
- is a dictionary
|
292
|
+
- has all required question_items as keys
|
293
|
+
- has values that are valid options from question_options
|
294
|
+
- has the correct number of responses (one per item)
|
295
|
+
"""
|
296
|
+
value = answer.get("answer")
|
297
|
+
|
298
|
+
# Check that answer is a dictionary
|
299
|
+
if not isinstance(value, dict):
|
300
|
+
raise QuestionAnswerValidationError(
|
301
|
+
f"Matrix answer must be a dictionary mapping items to responses (got {value})"
|
302
|
+
)
|
303
|
+
|
304
|
+
# Check that all required items are present
|
305
|
+
required_items = set(self.question_items)
|
306
|
+
provided_items = set(value.keys())
|
307
|
+
|
308
|
+
if missing_items := (required_items - provided_items):
|
309
|
+
raise QuestionAnswerValidationError(
|
310
|
+
f"Missing responses for items: {missing_items}"
|
311
|
+
)
|
312
|
+
|
313
|
+
if extra_items := (provided_items - required_items):
|
314
|
+
raise QuestionAnswerValidationError(
|
315
|
+
f"Unexpected responses for items: {extra_items}"
|
316
|
+
)
|
317
|
+
|
318
|
+
# Check that all responses are valid options
|
319
|
+
valid_options = set(self.question_options)
|
320
|
+
for item, response in value.items():
|
321
|
+
if response not in valid_options:
|
322
|
+
raise QuestionAnswerValidationError(
|
323
|
+
f"Invalid response '{response}' for item '{item}'. "
|
324
|
+
f"Must be one of: {valid_options}"
|
325
|
+
)
|
326
|
+
|
282
327
|
|
283
328
|
if __name__ == "__main__":
|
284
329
|
pass
|
@@ -0,0 +1,20 @@
|
|
1
|
+
from typing import Any, Optional, TypedDict
|
2
|
+
from pydantic import BaseModel
|
3
|
+
|
4
|
+
|
5
|
+
class RawEdslAnswerDict(TypedDict):
|
6
|
+
answer: Any
|
7
|
+
comment: Optional[str]
|
8
|
+
generated_tokens: Optional[str]
|
9
|
+
|
10
|
+
|
11
|
+
class BaseResponse(BaseModel):
|
12
|
+
answer: Any
|
13
|
+
comment: Optional[str] = None
|
14
|
+
generated_tokens: Optional[str] = None
|
15
|
+
|
16
|
+
|
17
|
+
class EdslAnswerDict(TypedDict):
|
18
|
+
answer: Any
|
19
|
+
comment: Optional[str]
|
20
|
+
generated_tokens: Optional[str]
|
@@ -40,9 +40,12 @@ class QuestionLinearScale(QuestionMultipleChoice):
|
|
40
40
|
include_comment=include_comment,
|
41
41
|
)
|
42
42
|
self.question_options = question_options
|
43
|
-
|
44
|
-
|
45
|
-
|
43
|
+
if isinstance(option_labels, str):
|
44
|
+
self.option_labels = option_labels
|
45
|
+
else:
|
46
|
+
self.option_labels = (
|
47
|
+
{int(k): v for k, v in option_labels.items()} if option_labels else {}
|
48
|
+
)
|
46
49
|
self.answering_instructions = answering_instructions
|
47
50
|
self.question_presentation = question_presentation
|
48
51
|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
from typing import Optional
|
3
3
|
|
4
|
-
from edsl.exceptions import QuestionCreationValidationError
|
4
|
+
from edsl.exceptions.questions import QuestionCreationValidationError
|
5
5
|
from edsl.questions.QuestionCheckBox import QuestionCheckBox
|
6
6
|
from edsl.questions.decorators import inject_exception
|
7
7
|
|
edsl/questions/descriptors.py
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
from abc import ABC, abstractmethod
|
4
4
|
import re
|
5
5
|
from typing import Any, Callable, List, Optional
|
6
|
-
from edsl.exceptions import (
|
6
|
+
from edsl.exceptions.questions import (
|
7
7
|
QuestionCreationValidationError,
|
8
8
|
QuestionAnswerValidationError,
|
9
9
|
)
|
@@ -181,11 +181,25 @@ class NumSelectionsDescriptor(BaseDescriptor):
|
|
181
181
|
|
182
182
|
|
183
183
|
class OptionLabelDescriptor(BaseDescriptor):
|
184
|
-
"""Validate that the `option_label` attribute is a string.
|
184
|
+
"""Validate that the `option_label` attribute is a string.
|
185
|
+
|
186
|
+
>>> class TestQuestion:
|
187
|
+
... option_label = OptionLabelDescriptor()
|
188
|
+
... def __init__(self, option_label: str):
|
189
|
+
... self.option_label = option_label
|
190
|
+
|
191
|
+
>>> _ = TestQuestion("{{Option}}")
|
192
|
+
|
193
|
+
"""
|
185
194
|
|
186
195
|
def validate(self, value, instance):
|
187
196
|
"""Validate the value is a string."""
|
188
|
-
|
197
|
+
if isinstance(value, str):
|
198
|
+
if "{{" in value and "}}" in value:
|
199
|
+
# they're trying to use a dynamic question name - let's let this play out
|
200
|
+
return None
|
201
|
+
|
202
|
+
key_values = [int(v) for v in value.keys()]
|
189
203
|
|
190
204
|
if value and (key_values := [float(v) for v in value.keys()]) != []:
|
191
205
|
if min(key_values) != min(instance.question_options):
|