edsl 0.1.39.dev1__py3-none-any.whl → 0.1.39.dev2__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 +169 -116
- edsl/__init__.py +14 -6
- edsl/__version__.py +1 -1
- edsl/agents/Agent.py +358 -146
- edsl/agents/AgentList.py +211 -73
- edsl/agents/Invigilator.py +88 -36
- edsl/agents/InvigilatorBase.py +59 -70
- edsl/agents/PromptConstructor.py +117 -219
- edsl/agents/QuestionInstructionPromptBuilder.py +128 -0
- edsl/agents/QuestionOptionProcessor.py +172 -0
- edsl/agents/QuestionTemplateReplacementsBuilder.py +137 -0
- edsl/agents/__init__.py +0 -1
- edsl/agents/prompt_helpers.py +3 -3
- 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 +104 -42
- edsl/coop/utils.py +14 -14
- edsl/data/Cache.py +21 -14
- edsl/data/CacheEntry.py +12 -15
- edsl/data/CacheHandler.py +33 -12
- edsl/data/__init__.py +4 -3
- edsl/data_transfer_models.py +2 -1
- edsl/enums.py +20 -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 +0 -3
- edsl/inference_services/AvailableModelCacheHandler.py +184 -0
- edsl/inference_services/AvailableModelFetcher.py +209 -0
- edsl/inference_services/AwsBedrock.py +0 -2
- edsl/inference_services/AzureAI.py +0 -2
- edsl/inference_services/GoogleService.py +2 -11
- edsl/inference_services/InferenceServiceABC.py +18 -85
- edsl/inference_services/InferenceServicesCollection.py +105 -80
- edsl/inference_services/MistralAIService.py +0 -3
- edsl/inference_services/OpenAIService.py +1 -4
- edsl/inference_services/PerplexityService.py +0 -3
- edsl/inference_services/ServiceAvailability.py +135 -0
- edsl/inference_services/TestService.py +11 -8
- edsl/inference_services/data_structures.py +62 -0
- edsl/jobs/AnswerQuestionFunctionConstructor.py +188 -0
- edsl/jobs/Answers.py +1 -14
- edsl/jobs/FetchInvigilator.py +40 -0
- edsl/jobs/InterviewTaskManager.py +98 -0
- edsl/jobs/InterviewsConstructor.py +48 -0
- edsl/jobs/Jobs.py +102 -243
- edsl/jobs/JobsChecks.py +35 -10
- edsl/jobs/JobsComponentConstructor.py +189 -0
- edsl/jobs/JobsPrompts.py +5 -3
- edsl/jobs/JobsRemoteInferenceHandler.py +128 -80
- edsl/jobs/JobsRemoteInferenceLogger.py +239 -0
- edsl/jobs/RequestTokenEstimator.py +30 -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/decorators.py +35 -0
- edsl/jobs/interviews/Interview.py +77 -380
- edsl/jobs/jobs_status_enums.py +9 -0
- edsl/jobs/loggers/HTMLTableJobLogger.py +304 -0
- edsl/jobs/runners/JobsRunnerAsyncio.py +4 -49
- edsl/jobs/tasks/QuestionTaskCreator.py +21 -19
- edsl/jobs/tasks/TaskHistory.py +14 -15
- edsl/jobs/tasks/task_status_enum.py +0 -2
- edsl/language_models/ComputeCost.py +63 -0
- edsl/language_models/LanguageModel.py +137 -234
- edsl/language_models/ModelList.py +11 -13
- 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 +0 -1
- 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/registry.py +49 -59
- 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/AnswerValidatorMixin.py +47 -2
- edsl/questions/ExceptionExplainer.py +77 -0
- edsl/questions/HTMLQuestion.py +103 -0
- edsl/questions/LoopProcessor.py +149 -0
- edsl/questions/QuestionBase.py +37 -192
- edsl/questions/QuestionBaseGenMixin.py +52 -48
- edsl/questions/QuestionBasePromptsMixin.py +7 -3
- edsl/questions/QuestionCheckBox.py +1 -1
- edsl/questions/QuestionExtract.py +1 -1
- edsl/questions/QuestionFreeText.py +1 -2
- edsl/questions/QuestionList.py +3 -5
- edsl/questions/QuestionMatrix.py +265 -0
- edsl/questions/QuestionMultipleChoice.py +66 -22
- edsl/questions/QuestionNumerical.py +1 -3
- edsl/questions/QuestionRank.py +6 -16
- edsl/questions/ResponseValidatorABC.py +37 -11
- edsl/questions/ResponseValidatorFactory.py +28 -0
- edsl/questions/SimpleAskMixin.py +4 -3
- edsl/questions/__init__.py +1 -0
- edsl/questions/derived/QuestionLinearScale.py +6 -3
- edsl/questions/derived/QuestionTopK.py +1 -1
- edsl/questions/descriptors.py +17 -3
- edsl/questions/question_registry.py +1 -1
- 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 +224 -302
- edsl/results/DatasetTree.py +28 -8
- edsl/results/MarkdownToDocx.py +122 -0
- edsl/results/MarkdownToPDF.py +111 -0
- edsl/results/Result.py +192 -206
- edsl/results/Results.py +120 -113
- edsl/results/ResultsExportMixin.py +2 -0
- edsl/results/Selector.py +23 -13
- edsl/results/TableDisplay.py +98 -171
- edsl/results/TextEditor.py +50 -0
- edsl/results/__init__.py +1 -1
- 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/DirectoryScanner.py +96 -0
- edsl/scenarios/DocumentChunker.py +102 -0
- edsl/scenarios/DocxScenario.py +16 -0
- edsl/scenarios/FileStore.py +118 -239
- edsl/scenarios/PdfExtractor.py +40 -0
- edsl/scenarios/Scenario.py +90 -193
- edsl/scenarios/ScenarioHtmlMixin.py +4 -3
- edsl/scenarios/ScenarioJoin.py +10 -6
- edsl/scenarios/ScenarioList.py +383 -240
- edsl/scenarios/ScenarioListExportMixin.py +0 -7
- edsl/scenarios/ScenarioListPdfMixin.py +15 -37
- edsl/scenarios/ScenarioSelector.py +156 -0
- edsl/scenarios/__init__.py +1 -2
- edsl/scenarios/file_methods.py +85 -0
- edsl/scenarios/handlers/__init__.py +13 -0
- edsl/scenarios/handlers/csv.py +38 -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/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 +199 -771
- 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.39.dev1.dist-info → edsl-0.1.39.dev2.dist-info}/METADATA +12 -10
- edsl-0.1.39.dev2.dist-info/RECORD +352 -0
- edsl/language_models/KeyLookup.py +0 -30
- edsl/language_models/unused/ReplicateBase.py +0 -83
- edsl/results/ResultsDBMixin.py +0 -238
- edsl-0.1.39.dev1.dist-info/RECORD +0 -277
- {edsl-0.1.39.dev1.dist-info → edsl-0.1.39.dev2.dist-info}/LICENSE +0 -0
- {edsl-0.1.39.dev1.dist-info → edsl-0.1.39.dev2.dist-info}/WHEEL +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.ResponseValidatorABC 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)
|
@@ -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
12
|
from edsl.questions.ResponseValidatorABC import ResponseValidatorABC
|
14
|
-
from edsl.exceptions.questions import QuestionAnswerValidationError
|
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
11
|
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
|
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 = [
|
@@ -1,11 +1,11 @@
|
|
1
|
+
import logging
|
1
2
|
from abc import ABC, abstractmethod
|
2
|
-
from pydantic import BaseModel, Field, field_validator
|
3
|
-
|
4
|
-
# from decimal import Decimal
|
5
3
|
from typing import Optional, Any, List, TypedDict
|
6
4
|
|
7
|
-
from
|
8
|
-
|
5
|
+
from pydantic import BaseModel, Field, field_validator, ValidationError
|
6
|
+
|
7
|
+
from edsl.exceptions.questions import QuestionAnswerValidationError
|
8
|
+
from edsl.questions.ExceptionExplainer import ExceptionExplainer
|
9
9
|
|
10
10
|
|
11
11
|
class BaseResponse(BaseModel):
|
@@ -72,7 +72,8 @@ class ResponseValidatorABC(ABC):
|
|
72
72
|
return self.override_answer if self.override_answer else data
|
73
73
|
|
74
74
|
def _base_validate(self, data: RawEdslAnswerDict) -> BaseModel:
|
75
|
-
"""This is the main validation function. It takes the response_model and checks the data against it,
|
75
|
+
"""This is the main validation function. It takes the response_model and checks the data against it,
|
76
|
+
returning the instantiated model.
|
76
77
|
|
77
78
|
>>> rv = ResponseValidatorABC.example("numerical")
|
78
79
|
>>> rv._base_validate({"answer": 42})
|
@@ -81,7 +82,9 @@ class ResponseValidatorABC(ABC):
|
|
81
82
|
try:
|
82
83
|
return self.response_model(**data)
|
83
84
|
except ValidationError as e:
|
84
|
-
raise QuestionAnswerValidationError(
|
85
|
+
raise QuestionAnswerValidationError(
|
86
|
+
message=str(e), pydantic_error=e, data=data, model=self.response_model
|
87
|
+
)
|
85
88
|
|
86
89
|
def post_validation_answer_convert(self, data):
|
87
90
|
return data
|
@@ -128,10 +131,13 @@ class ResponseValidatorABC(ABC):
|
|
128
131
|
edsl_answer_dict = self._extract_answer(pydantic_edsl_answer)
|
129
132
|
return self._post_process(edsl_answer_dict)
|
130
133
|
except QuestionAnswerValidationError as e:
|
131
|
-
if verbose:
|
132
|
-
print(f"Failed to validate {raw_edsl_answer_dict}; {str(e)}")
|
133
134
|
return self._handle_exception(e, raw_edsl_answer_dict)
|
134
135
|
|
136
|
+
def human_explanation(self, e: QuestionAnswerValidationError):
|
137
|
+
explanation = ExceptionExplainer(e, model_response=e.data).explain()
|
138
|
+
return explanation
|
139
|
+
# return e
|
140
|
+
|
135
141
|
def _handle_exception(self, e: Exception, raw_edsl_answer_dict) -> EdslAnswerDict:
|
136
142
|
if self.fixes_tried == 0:
|
137
143
|
self.original_exception = e
|
@@ -140,12 +146,18 @@ class ResponseValidatorABC(ABC):
|
|
140
146
|
self.fixes_tried += 1
|
141
147
|
fixed_data = self.fix(raw_edsl_answer_dict)
|
142
148
|
try:
|
143
|
-
return self.validate(fixed_data, fix=True)
|
149
|
+
return self.validate(fixed_data, fix=True) # early return if validates
|
144
150
|
except Exception as e:
|
145
151
|
pass # we don't log failed fixes
|
146
152
|
|
153
|
+
# If the exception is already a QuestionAnswerValidationError, raise it
|
154
|
+
if isinstance(self.original_exception, QuestionAnswerValidationError):
|
155
|
+
raise self.original_exception
|
156
|
+
|
157
|
+
# If nothing worked, raise the original exception
|
147
158
|
raise QuestionAnswerValidationError(
|
148
|
-
self.original_exception,
|
159
|
+
message=self.original_exception,
|
160
|
+
pydantic_error=self.original_exception,
|
149
161
|
data=raw_edsl_answer_dict,
|
150
162
|
model=self.response_model,
|
151
163
|
)
|
@@ -167,8 +179,22 @@ class ResponseValidatorABC(ABC):
|
|
167
179
|
return q.response_validator
|
168
180
|
|
169
181
|
|
182
|
+
def main():
|
183
|
+
rv = ResponseValidatorABC.example()
|
184
|
+
print(rv.validate({"answer": 42}))
|
185
|
+
|
186
|
+
|
170
187
|
# Example usage
|
171
188
|
if __name__ == "__main__":
|
172
189
|
import doctest
|
173
190
|
|
174
191
|
doctest.testmod(optionflags=doctest.ELLIPSIS)
|
192
|
+
|
193
|
+
rv = ResponseValidatorABC.example()
|
194
|
+
# print(rv.validate({"answer": 42}))
|
195
|
+
|
196
|
+
rv = ResponseValidatorABC.example()
|
197
|
+
try:
|
198
|
+
rv.validate({"answer": "120"})
|
199
|
+
except QuestionAnswerValidationError as e:
|
200
|
+
print(rv.human_explanation(e))
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class ResponseValidatorFactory:
|
2
|
+
def __init__(self, question):
|
3
|
+
self.question = question
|
4
|
+
|
5
|
+
@property
|
6
|
+
def response_model(self) -> type["BaseModel"]:
|
7
|
+
if self.question._response_model is not None:
|
8
|
+
return self.question._response_model
|
9
|
+
else:
|
10
|
+
return self.question.create_response_model()
|
11
|
+
|
12
|
+
@property
|
13
|
+
def response_validator(self) -> "ResponseValidatorBase":
|
14
|
+
"""Return the response validator."""
|
15
|
+
params = (
|
16
|
+
{
|
17
|
+
"response_model": self.question.response_model,
|
18
|
+
}
|
19
|
+
| {k: getattr(self.question, k) for k in self.validator_parameters}
|
20
|
+
| {"exception_to_throw": getattr(self.question, "exception_to_throw", None)}
|
21
|
+
| {"override_answer": getattr(self.question, "override_answer", None)}
|
22
|
+
)
|
23
|
+
return self.question.response_validator_class(**params)
|
24
|
+
|
25
|
+
@property
|
26
|
+
def validator_parameters(self) -> list[str]:
|
27
|
+
"""Return the parameters required for the response validator."""
|
28
|
+
return self.question.response_validator_class.required_params
|
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.registry import Model
|
69
70
|
|
70
71
|
if model is None:
|
71
72
|
model = Model()
|
edsl/questions/__init__.py
CHANGED
@@ -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
|
@@ -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):
|
@@ -96,7 +96,7 @@ class Question(metaclass=Meta):
|
|
96
96
|
|
97
97
|
>>> from edsl import Question
|
98
98
|
>>> Question.list_question_types()
|
99
|
-
['checkbox', 'extract', 'free_text', 'functional', 'likert_five', 'linear_scale', 'list', 'multiple_choice', 'numerical', 'rank', 'top_k', 'yes_no']
|
99
|
+
['checkbox', 'extract', 'free_text', 'functional', 'likert_five', 'linear_scale', 'list', 'matrix', 'multiple_choice', 'numerical', 'rank', 'top_k', 'yes_no']
|
100
100
|
"""
|
101
101
|
return [
|
102
102
|
q
|
@@ -0,0 +1 @@
|
|
1
|
+
|