edsl 0.1.49__py3-none-any.whl → 0.1.50__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/__init__.py +124 -53
- edsl/__version__.py +1 -1
- edsl/agents/agent.py +21 -21
- edsl/agents/agent_list.py +2 -5
- edsl/agents/exceptions.py +119 -5
- edsl/base/__init__.py +10 -35
- edsl/base/base_class.py +71 -36
- edsl/base/base_exception.py +204 -0
- edsl/base/data_transfer_models.py +1 -1
- edsl/base/exceptions.py +94 -0
- edsl/buckets/__init__.py +15 -1
- edsl/buckets/bucket_collection.py +3 -4
- edsl/buckets/exceptions.py +75 -0
- edsl/buckets/model_buckets.py +1 -2
- edsl/buckets/token_bucket.py +11 -6
- edsl/buckets/token_bucket_api.py +1 -2
- edsl/buckets/token_bucket_client.py +9 -7
- edsl/caching/cache.py +7 -2
- edsl/caching/cache_entry.py +10 -9
- edsl/caching/exceptions.py +113 -7
- edsl/caching/remote_cache_sync.py +1 -2
- edsl/caching/sql_dict.py +17 -12
- edsl/cli.py +43 -0
- edsl/config/config_class.py +30 -6
- edsl/conversation/Conversation.py +3 -2
- edsl/conversation/exceptions.py +58 -0
- edsl/conversation/mug_negotiation.py +0 -2
- edsl/coop/__init__.py +20 -1
- edsl/coop/coop.py +120 -29
- edsl/coop/exceptions.py +188 -9
- edsl/coop/price_fetcher.py +3 -6
- edsl/coop/utils.py +4 -6
- edsl/dataset/__init__.py +5 -4
- edsl/dataset/dataset.py +53 -43
- edsl/dataset/dataset_operations_mixin.py +86 -72
- edsl/dataset/dataset_tree.py +9 -5
- edsl/dataset/display/table_display.py +0 -2
- edsl/dataset/display/table_renderers.py +0 -1
- edsl/dataset/exceptions.py +125 -0
- edsl/dataset/file_exports.py +18 -11
- edsl/dataset/r/ggplot.py +13 -6
- edsl/display/__init__.py +27 -0
- edsl/display/core.py +147 -0
- edsl/display/plugin.py +189 -0
- edsl/display/utils.py +52 -0
- edsl/inference_services/__init__.py +9 -1
- edsl/inference_services/available_model_cache_handler.py +1 -1
- edsl/inference_services/available_model_fetcher.py +4 -5
- edsl/inference_services/data_structures.py +9 -6
- edsl/inference_services/exceptions.py +132 -1
- edsl/inference_services/inference_service_abc.py +2 -2
- edsl/inference_services/inference_services_collection.py +2 -6
- edsl/inference_services/registry.py +4 -3
- edsl/inference_services/service_availability.py +2 -1
- edsl/inference_services/services/anthropic_service.py +4 -1
- edsl/inference_services/services/aws_bedrock.py +13 -12
- edsl/inference_services/services/azure_ai.py +12 -10
- edsl/inference_services/services/deep_infra_service.py +1 -4
- edsl/inference_services/services/deep_seek_service.py +1 -5
- edsl/inference_services/services/google_service.py +6 -2
- edsl/inference_services/services/groq_service.py +1 -1
- edsl/inference_services/services/mistral_ai_service.py +4 -2
- edsl/inference_services/services/ollama_service.py +1 -1
- edsl/inference_services/services/open_ai_service.py +7 -5
- edsl/inference_services/services/perplexity_service.py +6 -2
- edsl/inference_services/services/test_service.py +8 -7
- edsl/inference_services/services/together_ai_service.py +2 -3
- edsl/inference_services/services/xai_service.py +1 -1
- edsl/instructions/__init__.py +1 -1
- edsl/instructions/change_instruction.py +3 -2
- edsl/instructions/exceptions.py +61 -0
- edsl/instructions/instruction.py +5 -2
- edsl/instructions/instruction_collection.py +2 -1
- edsl/instructions/instruction_handler.py +4 -9
- edsl/interviews/ReportErrors.py +0 -3
- edsl/interviews/__init__.py +9 -2
- edsl/interviews/answering_function.py +11 -13
- edsl/interviews/exception_tracking.py +14 -7
- edsl/interviews/exceptions.py +79 -0
- edsl/interviews/interview.py +32 -29
- edsl/interviews/interview_status_dictionary.py +4 -2
- edsl/interviews/interview_status_log.py +2 -1
- edsl/interviews/interview_task_manager.py +3 -3
- edsl/interviews/request_token_estimator.py +3 -1
- edsl/interviews/statistics.py +2 -3
- edsl/invigilators/__init__.py +7 -1
- edsl/invigilators/exceptions.py +79 -0
- edsl/invigilators/invigilator_base.py +0 -1
- edsl/invigilators/invigilators.py +8 -12
- edsl/invigilators/prompt_constructor.py +1 -5
- edsl/invigilators/prompt_helpers.py +8 -4
- edsl/invigilators/question_instructions_prompt_builder.py +1 -1
- edsl/invigilators/question_option_processor.py +9 -5
- edsl/invigilators/question_template_replacements_builder.py +3 -2
- edsl/jobs/__init__.py +3 -3
- edsl/jobs/async_interview_runner.py +24 -22
- edsl/jobs/check_survey_scenario_compatibility.py +7 -6
- edsl/jobs/data_structures.py +7 -4
- edsl/jobs/exceptions.py +177 -8
- edsl/jobs/fetch_invigilator.py +1 -1
- edsl/jobs/jobs.py +72 -67
- edsl/jobs/jobs_checks.py +2 -3
- edsl/jobs/jobs_component_constructor.py +2 -2
- edsl/jobs/jobs_pricing_estimation.py +3 -2
- edsl/jobs/jobs_remote_inference_logger.py +5 -4
- edsl/jobs/jobs_runner_asyncio.py +1 -2
- edsl/jobs/jobs_runner_status.py +8 -9
- edsl/jobs/remote_inference.py +26 -23
- edsl/jobs/results_exceptions_handler.py +8 -5
- edsl/key_management/__init__.py +3 -1
- edsl/key_management/exceptions.py +62 -0
- edsl/key_management/key_lookup.py +1 -1
- edsl/key_management/key_lookup_builder.py +37 -14
- edsl/key_management/key_lookup_collection.py +2 -0
- edsl/language_models/__init__.py +1 -1
- edsl/language_models/exceptions.py +302 -14
- edsl/language_models/language_model.py +4 -7
- edsl/language_models/model.py +4 -4
- edsl/language_models/model_list.py +1 -1
- edsl/language_models/price_manager.py +1 -1
- edsl/language_models/raw_response_handler.py +14 -9
- edsl/language_models/registry.py +17 -21
- edsl/language_models/repair.py +0 -6
- edsl/language_models/unused/fake_openai_service.py +0 -1
- edsl/load_plugins.py +69 -0
- edsl/logger.py +146 -0
- edsl/notebooks/notebook.py +1 -1
- edsl/notebooks/notebook_to_latex.py +0 -1
- edsl/plugins/__init__.py +63 -0
- edsl/plugins/built_in/export_example.py +50 -0
- edsl/plugins/built_in/pig_latin.py +67 -0
- edsl/plugins/cli.py +372 -0
- edsl/plugins/cli_typer.py +283 -0
- edsl/plugins/exceptions.py +31 -0
- edsl/plugins/hookspec.py +51 -0
- edsl/plugins/plugin_host.py +128 -0
- edsl/plugins/plugin_manager.py +633 -0
- edsl/plugins/plugins_registry.py +168 -0
- edsl/prompts/__init__.py +2 -0
- edsl/prompts/exceptions.py +107 -5
- edsl/prompts/prompt.py +14 -6
- edsl/questions/HTMLQuestion.py +5 -11
- edsl/questions/Quick.py +0 -1
- edsl/questions/__init__.py +2 -0
- edsl/questions/answer_validator_mixin.py +318 -318
- edsl/questions/compose_questions.py +2 -2
- edsl/questions/descriptors.py +10 -49
- edsl/questions/exceptions.py +278 -22
- edsl/questions/loop_processor.py +7 -5
- edsl/questions/prompt_templates/question_list.jinja +3 -0
- edsl/questions/question_base.py +14 -16
- edsl/questions/question_base_gen_mixin.py +2 -2
- edsl/questions/question_base_prompts_mixin.py +9 -3
- edsl/questions/question_budget.py +9 -5
- edsl/questions/question_check_box.py +3 -5
- edsl/questions/question_dict.py +171 -194
- edsl/questions/question_extract.py +1 -1
- edsl/questions/question_free_text.py +4 -6
- edsl/questions/question_functional.py +4 -3
- edsl/questions/question_list.py +36 -9
- edsl/questions/question_matrix.py +95 -61
- edsl/questions/question_multiple_choice.py +6 -4
- edsl/questions/question_numerical.py +2 -4
- edsl/questions/question_registry.py +4 -2
- edsl/questions/register_questions_meta.py +0 -1
- edsl/questions/response_validator_abc.py +7 -13
- edsl/questions/templates/dict/answering_instructions.jinja +1 -0
- edsl/questions/templates/rank/question_presentation.jinja +1 -1
- edsl/results/__init__.py +1 -1
- edsl/results/exceptions.py +141 -7
- edsl/results/report.py +0 -1
- edsl/results/result.py +4 -5
- edsl/results/results.py +10 -51
- edsl/results/results_selector.py +8 -4
- edsl/scenarios/PdfExtractor.py +2 -2
- edsl/scenarios/construct_download_link.py +69 -35
- edsl/scenarios/directory_scanner.py +33 -14
- edsl/scenarios/document_chunker.py +1 -1
- edsl/scenarios/exceptions.py +238 -14
- edsl/scenarios/file_methods.py +1 -1
- edsl/scenarios/file_store.py +7 -3
- edsl/scenarios/handlers/__init__.py +17 -0
- edsl/scenarios/handlers/docx_file_store.py +0 -5
- edsl/scenarios/handlers/pdf_file_store.py +0 -1
- edsl/scenarios/handlers/pptx_file_store.py +0 -5
- edsl/scenarios/handlers/py_file_store.py +0 -1
- edsl/scenarios/handlers/sql_file_store.py +1 -4
- edsl/scenarios/handlers/sqlite_file_store.py +0 -1
- edsl/scenarios/handlers/txt_file_store.py +1 -1
- edsl/scenarios/scenario.py +0 -1
- edsl/scenarios/scenario_list.py +152 -18
- edsl/scenarios/scenario_list_pdf_tools.py +1 -0
- edsl/scenarios/scenario_selector.py +0 -1
- edsl/surveys/__init__.py +3 -4
- edsl/surveys/dag/__init__.py +4 -2
- edsl/surveys/descriptors.py +1 -1
- edsl/surveys/edit_survey.py +1 -0
- edsl/surveys/exceptions.py +165 -9
- edsl/surveys/memory/__init__.py +5 -3
- edsl/surveys/memory/memory_management.py +1 -0
- edsl/surveys/memory/memory_plan.py +6 -15
- edsl/surveys/rules/__init__.py +5 -3
- edsl/surveys/rules/rule.py +1 -2
- edsl/surveys/rules/rule_collection.py +1 -1
- edsl/surveys/survey.py +12 -24
- edsl/surveys/survey_export.py +6 -3
- edsl/surveys/survey_flow_visualization.py +10 -1
- edsl/tasks/__init__.py +2 -0
- edsl/tasks/question_task_creator.py +3 -3
- edsl/tasks/task_creators.py +1 -3
- edsl/tasks/task_history.py +5 -7
- edsl/tasks/task_status_log.py +1 -2
- edsl/tokens/__init__.py +3 -1
- edsl/tokens/token_usage.py +1 -1
- edsl/utilities/__init__.py +21 -1
- edsl/utilities/decorators.py +1 -2
- edsl/utilities/markdown_to_docx.py +2 -2
- edsl/utilities/markdown_to_pdf.py +1 -1
- edsl/utilities/repair_functions.py +0 -1
- edsl/utilities/restricted_python.py +0 -1
- edsl/utilities/template_loader.py +2 -3
- edsl/utilities/utilities.py +8 -29
- {edsl-0.1.49.dist-info → edsl-0.1.50.dist-info}/METADATA +32 -2
- edsl-0.1.50.dist-info/RECORD +363 -0
- edsl-0.1.50.dist-info/entry_points.txt +3 -0
- edsl/dataset/smart_objects.py +0 -96
- edsl/exceptions/BaseException.py +0 -21
- edsl/exceptions/__init__.py +0 -54
- edsl/exceptions/configuration.py +0 -16
- edsl/exceptions/general.py +0 -34
- edsl/study/ObjectEntry.py +0 -173
- edsl/study/ProofOfWork.py +0 -113
- edsl/study/SnapShot.py +0 -80
- edsl/study/Study.py +0 -520
- edsl/study/__init__.py +0 -6
- edsl/utilities/interface.py +0 -135
- edsl-0.1.49.dist-info/RECORD +0 -347
- {edsl-0.1.49.dist-info → edsl-0.1.50.dist-info}/LICENSE +0 -0
- {edsl-0.1.49.dist-info → edsl-0.1.50.dist-info}/WHEEL +0 -0
edsl/questions/question_list.py
CHANGED
@@ -1,16 +1,18 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
import json
|
3
|
-
from typing import Any, Optional, Union
|
3
|
+
from typing import Any, Optional, Union, ForwardRef
|
4
4
|
|
5
5
|
from pydantic import Field
|
6
6
|
from json_repair import repair_json
|
7
|
-
|
8
7
|
from .exceptions import QuestionAnswerValidationError
|
9
8
|
from .question_base import QuestionBase
|
10
9
|
from .descriptors import IntegerOrNoneDescriptor
|
11
10
|
from .decorators import inject_exception
|
12
11
|
from .response_validator_abc import ResponseValidatorABC
|
13
12
|
|
13
|
+
# Forward reference for function return type annotation
|
14
|
+
ListResponse = ForwardRef("ListResponse")
|
15
|
+
|
14
16
|
def convert_string(s: str) -> Union[float, int, str, dict]:
|
15
17
|
"""Convert a string to a more appropriate type if possible.
|
16
18
|
|
@@ -54,10 +56,10 @@ def convert_string(s: str) -> Union[float, int, str, dict]:
|
|
54
56
|
return s
|
55
57
|
|
56
58
|
|
57
|
-
def create_model(max_list_items: int, permissive: bool) -> "ListResponse":
|
59
|
+
def create_model(min_list_items: Optional[int], max_list_items: Optional[int], permissive: bool) -> "ListResponse":
|
58
60
|
from pydantic import BaseModel
|
59
61
|
|
60
|
-
if permissive or max_list_items is None:
|
62
|
+
if permissive or (max_list_items is None and min_list_items is None):
|
61
63
|
|
62
64
|
class ListResponse(BaseModel):
|
63
65
|
answer: list[Any]
|
@@ -65,6 +67,14 @@ def create_model(max_list_items: int, permissive: bool) -> "ListResponse":
|
|
65
67
|
generated_tokens: Optional[str] = None
|
66
68
|
|
67
69
|
else:
|
70
|
+
# Determine field constraints
|
71
|
+
field_kwargs = {"...": None}
|
72
|
+
|
73
|
+
if min_list_items is not None:
|
74
|
+
field_kwargs["min_items"] = min_list_items
|
75
|
+
|
76
|
+
if max_list_items is not None:
|
77
|
+
field_kwargs["max_items"] = max_list_items
|
68
78
|
|
69
79
|
class ListResponse(BaseModel):
|
70
80
|
"""
|
@@ -73,7 +83,7 @@ def create_model(max_list_items: int, permissive: bool) -> "ListResponse":
|
|
73
83
|
{'answer': ['Apple', 'Cherry'], 'comment': None, 'generated_tokens': None}
|
74
84
|
"""
|
75
85
|
|
76
|
-
answer: list[Any] = Field(
|
86
|
+
answer: list[Any] = Field(**field_kwargs)
|
77
87
|
comment: Optional[str] = None
|
78
88
|
generated_tokens: Optional[str] = None
|
79
89
|
|
@@ -81,7 +91,7 @@ def create_model(max_list_items: int, permissive: bool) -> "ListResponse":
|
|
81
91
|
|
82
92
|
|
83
93
|
class ListResponseValidator(ResponseValidatorABC):
|
84
|
-
required_params = ["max_list_items", "permissive"]
|
94
|
+
required_params = ["min_list_items", "max_list_items", "permissive"]
|
85
95
|
valid_examples = [({"answer": ["hello", "world"]}, {"max_list_items": 5})]
|
86
96
|
|
87
97
|
invalid_examples = [
|
@@ -90,6 +100,11 @@ class ListResponseValidator(ResponseValidatorABC):
|
|
90
100
|
{"max_list_items": 5},
|
91
101
|
"Too many items.",
|
92
102
|
),
|
103
|
+
(
|
104
|
+
{"answer": ["hello"]},
|
105
|
+
{"min_list_items": 2},
|
106
|
+
"Too few items.",
|
107
|
+
),
|
93
108
|
]
|
94
109
|
|
95
110
|
def _check_constraints(self, response) -> None:
|
@@ -98,6 +113,12 @@ class ListResponseValidator(ResponseValidatorABC):
|
|
98
113
|
and len(response.answer) > self.max_list_items
|
99
114
|
):
|
100
115
|
raise QuestionAnswerValidationError("Too many items.")
|
116
|
+
|
117
|
+
if (
|
118
|
+
self.min_list_items is not None
|
119
|
+
and len(response.answer) < self.min_list_items
|
120
|
+
):
|
121
|
+
raise QuestionAnswerValidationError("Too few items.")
|
101
122
|
|
102
123
|
def fix(self, response, verbose=False):
|
103
124
|
if verbose:
|
@@ -122,6 +143,7 @@ class QuestionList(QuestionBase):
|
|
122
143
|
|
123
144
|
question_type = "list"
|
124
145
|
max_list_items: int = IntegerOrNoneDescriptor()
|
146
|
+
min_list_items: int = IntegerOrNoneDescriptor()
|
125
147
|
_response_model = None
|
126
148
|
response_validator_class = ListResponseValidator
|
127
149
|
|
@@ -131,6 +153,7 @@ class QuestionList(QuestionBase):
|
|
131
153
|
question_text: str,
|
132
154
|
include_comment: bool = True,
|
133
155
|
max_list_items: Optional[int] = None,
|
156
|
+
min_list_items: Optional[int] = None,
|
134
157
|
answering_instructions: Optional[str] = None,
|
135
158
|
question_presentation: Optional[str] = None,
|
136
159
|
permissive: bool = False,
|
@@ -140,12 +163,14 @@ class QuestionList(QuestionBase):
|
|
140
163
|
:param question_name: The name of the question.
|
141
164
|
:param question_text: The text of the question.
|
142
165
|
:param max_list_items: The maximum number of items that can be in the answer list.
|
166
|
+
:param min_list_items: The minimum number of items that must be in the answer list.
|
143
167
|
|
144
168
|
>>> QuestionList.example().self_check()
|
145
169
|
"""
|
146
170
|
self.question_name = question_name
|
147
171
|
self.question_text = question_text
|
148
172
|
self.max_list_items = max_list_items
|
173
|
+
self.min_list_items = min_list_items
|
149
174
|
self.permissive = permissive
|
150
175
|
|
151
176
|
self.include_comment = include_comment
|
@@ -153,7 +178,7 @@ class QuestionList(QuestionBase):
|
|
153
178
|
self.question_presentations = question_presentation
|
154
179
|
|
155
180
|
def create_response_model(self):
|
156
|
-
return create_model(self.max_list_items, self.permissive)
|
181
|
+
return create_model(self.min_list_items, self.max_list_items, self.permissive)
|
157
182
|
|
158
183
|
@property
|
159
184
|
def question_html_content(self) -> str:
|
@@ -183,7 +208,7 @@ class QuestionList(QuestionBase):
|
|
183
208
|
@classmethod
|
184
209
|
@inject_exception
|
185
210
|
def example(
|
186
|
-
cls, include_comment=True, max_list_items=None, permissive=False
|
211
|
+
cls, include_comment=True, max_list_items=None, min_list_items=None, permissive=False
|
187
212
|
) -> QuestionList:
|
188
213
|
"""Return an example of a list question."""
|
189
214
|
return cls(
|
@@ -191,6 +216,7 @@ class QuestionList(QuestionBase):
|
|
191
216
|
question_text="What are your favorite foods?",
|
192
217
|
include_comment=include_comment,
|
193
218
|
max_list_items=max_list_items,
|
219
|
+
min_list_items=min_list_items,
|
194
220
|
permissive=permissive,
|
195
221
|
)
|
196
222
|
|
@@ -199,10 +225,11 @@ def main():
|
|
199
225
|
"""Create an example of a list question and demonstrate its functionality."""
|
200
226
|
from edsl.questions import QuestionList
|
201
227
|
|
202
|
-
q = QuestionList.example(max_list_items=5)
|
228
|
+
q = QuestionList.example(max_list_items=5, min_list_items=2)
|
203
229
|
q.question_text
|
204
230
|
q.question_name
|
205
231
|
q.max_list_items
|
232
|
+
q.min_list_items
|
206
233
|
# validate an answer
|
207
234
|
q._validate_answer({"answer": ["pasta", "garlic", "oil", "parmesan"]})
|
208
235
|
# translate answer code
|
@@ -1,9 +1,26 @@
|
|
1
|
+
"""
|
2
|
+
question_matrix.py
|
3
|
+
|
4
|
+
Drop-in replacement for `QuestionMatrix` with a dynamic Pydantic approach
|
5
|
+
that automatically raises ValidationError for invalid matrix answers.
|
6
|
+
"""
|
7
|
+
|
1
8
|
from __future__ import annotations
|
2
|
-
from typing import
|
9
|
+
from typing import (
|
10
|
+
Union,
|
11
|
+
Optional,
|
12
|
+
Dict,
|
13
|
+
List,
|
14
|
+
Any,
|
15
|
+
Type,
|
16
|
+
get_args,
|
17
|
+
Literal
|
18
|
+
)
|
19
|
+
import random
|
3
20
|
|
4
|
-
from pydantic import BaseModel, Field,
|
21
|
+
from pydantic import BaseModel, Field, create_model, ValidationError
|
5
22
|
from jinja2 import Template
|
6
|
-
|
23
|
+
|
7
24
|
from .question_base import QuestionBase
|
8
25
|
from .descriptors import (
|
9
26
|
QuestionOptionsDescriptor,
|
@@ -14,8 +31,8 @@ from .response_validator_abc import ResponseValidatorABC
|
|
14
31
|
from .decorators import inject_exception
|
15
32
|
|
16
33
|
from .exceptions import (
|
17
|
-
QuestionAnswerValidationError,
|
18
34
|
QuestionCreationValidationError,
|
35
|
+
QuestionAnswerValidationError, # If you still want to raise custom exceptions
|
19
36
|
)
|
20
37
|
|
21
38
|
|
@@ -23,47 +40,56 @@ def create_matrix_response(
|
|
23
40
|
question_items: List[str],
|
24
41
|
question_options: List[Union[int, str, float]],
|
25
42
|
permissive: bool = False,
|
26
|
-
):
|
27
|
-
"""Create a response model for matrix questions.
|
28
|
-
|
29
|
-
The response model validates that:
|
30
|
-
1. All question items are answered
|
31
|
-
2. Each answer is from the allowed options
|
43
|
+
) -> Type[BaseModel]:
|
32
44
|
"""
|
45
|
+
Create a dynamic Pydantic model for matrix questions.
|
33
46
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
comment: Optional[str] = None
|
39
|
-
generated_tokens: Optional[Any] = None
|
47
|
+
If `permissive=False`, each item is a required field with a `Literal[...]` type
|
48
|
+
so that only the given question_options are allowed.
|
49
|
+
If `permissive=True`, each item can have any value, and extra items are allowed.
|
50
|
+
"""
|
40
51
|
|
52
|
+
# If non-permissive, build a Literal for each valid option
|
53
|
+
# e.g. Literal[1,2,3] or Literal["Yes","No"] or a mix
|
54
|
+
if not permissive:
|
55
|
+
# If question_options is empty (edge case), fall back to 'Any'
|
56
|
+
if question_options:
|
57
|
+
AllowedOptions = Literal[tuple(question_options)] # type: ignore
|
58
|
+
else:
|
59
|
+
AllowedOptions = Any
|
41
60
|
else:
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
61
|
+
# Permissive => let each item be anything
|
62
|
+
AllowedOptions = Any
|
63
|
+
|
64
|
+
# Build field definitions for an "AnswerSubModel", where each
|
65
|
+
# question_item is a required field with type AllowedOptions
|
66
|
+
field_definitions = {}
|
67
|
+
for item in question_items:
|
68
|
+
field_definitions[item] = (AllowedOptions, Field(...)) # required
|
69
|
+
|
70
|
+
# Dynamically create the submodel
|
71
|
+
MatrixAnswerSubModel = create_model(
|
72
|
+
"MatrixAnswerSubModel",
|
73
|
+
__base__=BaseModel,
|
74
|
+
**field_definitions
|
75
|
+
)
|
76
|
+
|
77
|
+
# Build the top-level model with `answer` + optional `comment`
|
78
|
+
class MatrixResponse(BaseModel):
|
79
|
+
answer: MatrixAnswerSubModel
|
80
|
+
comment: Optional[str] = None
|
81
|
+
generated_tokens: Optional[Any] = None
|
82
|
+
|
83
|
+
class Config:
|
84
|
+
# If permissive=False, forbid extra items in `answer`.
|
85
|
+
# If permissive=True, allow them.
|
86
|
+
extra = "allow" if permissive else "forbid"
|
62
87
|
|
63
88
|
return MatrixResponse
|
64
89
|
|
65
90
|
|
66
91
|
class MatrixResponseValidator(ResponseValidatorABC):
|
92
|
+
"""Optional placeholder validator, if still needed for example/fixing logic."""
|
67
93
|
required_params = ["question_items", "question_options", "permissive"]
|
68
94
|
|
69
95
|
valid_examples = [
|
@@ -96,6 +122,10 @@ class MatrixResponseValidator(ResponseValidatorABC):
|
|
96
122
|
]
|
97
123
|
|
98
124
|
def fix(self, response, verbose=False):
|
125
|
+
"""
|
126
|
+
Example fix() method to try and repair a partially invalid response.
|
127
|
+
(This logic is carried over from your original code.)
|
128
|
+
"""
|
99
129
|
if verbose:
|
100
130
|
print(f"Fixing matrix response: {response}")
|
101
131
|
|
@@ -111,12 +141,10 @@ class MatrixResponseValidator(ResponseValidatorABC):
|
|
111
141
|
for idx, item in enumerate(self.question_items):
|
112
142
|
if str(idx) in fixed:
|
113
143
|
mapped_answer[item] = fixed[str(idx)]
|
114
|
-
if
|
115
|
-
mapped_answer
|
116
|
-
): # Only return if we successfully mapped some answers
|
144
|
+
if mapped_answer:
|
117
145
|
return {"answer": mapped_answer}
|
118
|
-
except:
|
119
|
-
pass
|
146
|
+
except (ValueError, KeyError, TypeError):
|
147
|
+
pass # Just continue
|
120
148
|
|
121
149
|
# If answer uses numeric keys, map them to question items
|
122
150
|
if "answer" in response and isinstance(response["answer"], dict):
|
@@ -125,14 +153,21 @@ class MatrixResponseValidator(ResponseValidatorABC):
|
|
125
153
|
for idx, item in enumerate(self.question_items):
|
126
154
|
if str(idx) in response["answer"]:
|
127
155
|
mapped_answer[item] = response["answer"][str(idx)]
|
128
|
-
if mapped_answer:
|
156
|
+
if mapped_answer:
|
129
157
|
response["answer"] = mapped_answer
|
130
158
|
|
131
159
|
return response
|
132
160
|
|
133
161
|
|
134
162
|
class QuestionMatrix(QuestionBase):
|
135
|
-
"""
|
163
|
+
"""
|
164
|
+
A question that presents a matrix/grid where multiple items are rated
|
165
|
+
or selected from the same set of options.
|
166
|
+
|
167
|
+
This version dynamically builds a Pydantic model at runtime
|
168
|
+
(via `create_matrix_response`) and automatically raises ValidationError
|
169
|
+
if the user provides an invalid or incomplete answer.
|
170
|
+
"""
|
136
171
|
|
137
172
|
question_type = "matrix"
|
138
173
|
question_text: str = QuestionTextDescriptor()
|
@@ -155,18 +190,19 @@ class QuestionMatrix(QuestionBase):
|
|
155
190
|
question_presentation: Optional[str] = None,
|
156
191
|
permissive: bool = False,
|
157
192
|
):
|
158
|
-
"""
|
193
|
+
"""
|
194
|
+
Initialize a matrix question.
|
159
195
|
|
160
196
|
Args:
|
161
197
|
question_name: The name of the question
|
162
198
|
question_text: The text of the question
|
163
|
-
question_items: List of items to be rated
|
164
|
-
question_options:
|
165
|
-
option_labels: Optional mapping of options to
|
199
|
+
question_items: List of items to be rated or answered
|
200
|
+
question_options: Possible answer options (e.g., [1,2,3] or ["Yes","No"])
|
201
|
+
option_labels: Optional mapping of options to labels (e.g. {1: "Sad", 5: "Happy"})
|
166
202
|
include_comment: Whether to include a comment field
|
167
|
-
answering_instructions:
|
168
|
-
question_presentation:
|
169
|
-
permissive: Whether to strictly
|
203
|
+
answering_instructions: Custom instructions
|
204
|
+
question_presentation: Custom presentation
|
205
|
+
permissive: Whether to allow any values & extra items instead of strictly checking
|
170
206
|
"""
|
171
207
|
self.question_name = question_name
|
172
208
|
|
@@ -186,9 +222,14 @@ class QuestionMatrix(QuestionBase):
|
|
186
222
|
self.question_presentation = question_presentation
|
187
223
|
self.permissive = permissive
|
188
224
|
|
189
|
-
def create_response_model(self):
|
225
|
+
def create_response_model(self) -> Type[BaseModel]:
|
226
|
+
"""
|
227
|
+
Returns the pydantic model that will parse/validate a user answer.
|
228
|
+
"""
|
190
229
|
return create_matrix_response(
|
191
|
-
self.question_items,
|
230
|
+
self.question_items,
|
231
|
+
self.question_options,
|
232
|
+
self.permissive
|
192
233
|
)
|
193
234
|
|
194
235
|
@property
|
@@ -225,7 +266,6 @@ class QuestionMatrix(QuestionBase):
|
|
225
266
|
</table>
|
226
267
|
"""
|
227
268
|
)
|
228
|
-
|
229
269
|
return template.render(
|
230
270
|
question_name=self.question_name,
|
231
271
|
question_items=self.question_items,
|
@@ -256,11 +296,5 @@ class QuestionMatrix(QuestionBase):
|
|
256
296
|
"answer": {
|
257
297
|
item: random.choice(self.question_options)
|
258
298
|
for item in self.question_items
|
259
|
-
}
|
260
|
-
}
|
261
|
-
|
262
|
-
|
263
|
-
if __name__ == "__main__":
|
264
|
-
import doctest
|
265
|
-
|
266
|
-
doctest.testmod(optionflags=doctest.ELLIPSIS)
|
299
|
+
}
|
300
|
+
}
|
@@ -4,7 +4,6 @@ from typing import Union, Literal, Optional, List, Any
|
|
4
4
|
from jinja2 import Template
|
5
5
|
from pydantic import BaseModel, Field
|
6
6
|
|
7
|
-
from ..scenarios import Scenario
|
8
7
|
from .question_base import QuestionBase
|
9
8
|
from .descriptors import QuestionOptionsDescriptor
|
10
9
|
from .decorators import inject_exception
|
@@ -314,7 +313,8 @@ class QuestionMultipleChoice(QuestionBase):
|
|
314
313
|
|
315
314
|
if potential_replacement is None:
|
316
315
|
# Nope - maybe it's in the substition dict?
|
317
|
-
|
316
|
+
from .exceptions import QuestionValueError
|
317
|
+
raise QuestionValueError(
|
318
318
|
f"Could not find the key '{question_option_key}' in the scenario."
|
319
319
|
f"The substition dict was: '{substitution_dict}.'"
|
320
320
|
f"The question options were: '{question_options}'."
|
@@ -353,11 +353,13 @@ class QuestionMultipleChoice(QuestionBase):
|
|
353
353
|
try:
|
354
354
|
return translated_options[int(answer_code)]
|
355
355
|
except IndexError:
|
356
|
-
|
356
|
+
from .exceptions import QuestionValueError
|
357
|
+
raise QuestionValueError(
|
357
358
|
f"Answer code is out of range. The answer code index was: {int(answer_code)}. The options were: {translated_options}."
|
358
359
|
)
|
359
360
|
except TypeError:
|
360
|
-
|
361
|
+
from .exceptions import QuestionValueError
|
362
|
+
raise QuestionValueError(
|
361
363
|
f"The answer code was: '{answer_code}.'",
|
362
364
|
f"The options were: '{translated_options}'.",
|
363
365
|
)
|
@@ -1,11 +1,9 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
from
|
4
|
-
from typing import Any, Optional, Union, Literal
|
3
|
+
from typing import Any, Optional, Union
|
5
4
|
|
6
|
-
from pydantic import BaseModel, Field
|
5
|
+
from pydantic import BaseModel, Field
|
7
6
|
|
8
|
-
from .exceptions import QuestionAnswerValidationError
|
9
7
|
from .question_base import QuestionBase
|
10
8
|
from .descriptors import NumericalOrNoneDescriptor
|
11
9
|
from .decorators import inject_exception
|
@@ -42,7 +42,8 @@ class Question(metaclass=Meta):
|
|
42
42
|
|
43
43
|
subclass = get_question_classes.get(question_type, None)
|
44
44
|
if subclass is None:
|
45
|
-
|
45
|
+
from .exceptions import QuestionValueError
|
46
|
+
raise QuestionValueError(
|
46
47
|
f"No question registered with question_type {question_type}"
|
47
48
|
)
|
48
49
|
|
@@ -144,7 +145,8 @@ def get_question_class(question_type):
|
|
144
145
|
"""Return the class for the given question type."""
|
145
146
|
q2c = RegisterQuestionsMeta.question_types_to_classes()
|
146
147
|
if question_type not in q2c:
|
147
|
-
|
148
|
+
from .exceptions import QuestionValueError
|
149
|
+
raise QuestionValueError(
|
148
150
|
f"The question type, {question_type}, is not recognized. Recognied types are: {q2c.keys()}"
|
149
151
|
)
|
150
152
|
return q2c.get(question_type)
|
@@ -1,7 +1,7 @@
|
|
1
|
-
from abc import ABC
|
2
|
-
from typing import Optional,
|
1
|
+
from abc import ABC
|
2
|
+
from typing import Optional, List, TYPE_CHECKING
|
3
3
|
|
4
|
-
from pydantic import BaseModel,
|
4
|
+
from pydantic import BaseModel, ValidationError
|
5
5
|
|
6
6
|
from .exceptions import QuestionAnswerValidationError
|
7
7
|
from .ExceptionExplainer import ExceptionExplainer
|
@@ -22,7 +22,8 @@ class ResponseValidatorABC(ABC):
|
|
22
22
|
required_class_vars = ["required_params", "valid_examples", "invalid_examples"]
|
23
23
|
for var in required_class_vars:
|
24
24
|
if not hasattr(cls, var):
|
25
|
-
|
25
|
+
from .exceptions import QuestionValueError
|
26
|
+
raise QuestionValueError(f"Class {cls.__name__} must have a '{var}' attribute.")
|
26
27
|
|
27
28
|
def __init__(
|
28
29
|
self,
|
@@ -41,7 +42,8 @@ class ResponseValidatorABC(ABC):
|
|
41
42
|
param for param in self.required_params if param not in kwargs
|
42
43
|
]
|
43
44
|
if missing_params:
|
44
|
-
|
45
|
+
from .exceptions import QuestionValueError
|
46
|
+
raise QuestionValueError(
|
45
47
|
f"Missing required parameters: {', '.join(missing_params)}"
|
46
48
|
)
|
47
49
|
|
@@ -98,20 +100,12 @@ class ResponseValidatorABC(ABC):
|
|
98
100
|
{'answer': 42, 'comment': None, 'generated_tokens': None}
|
99
101
|
>>> rv.max_value
|
100
102
|
86.7
|
101
|
-
>>> rv.validate({"answer": "120"})
|
102
|
-
Traceback (most recent call last):
|
103
|
-
...
|
104
|
-
edsl.questions.exceptions.QuestionAnswerValidationError:...
|
105
103
|
>>> from edsl import QuestionNumerical
|
106
104
|
>>> q = QuestionNumerical.example()
|
107
105
|
>>> q.permissive = True
|
108
106
|
>>> rv = q.response_validator
|
109
107
|
>>> rv.validate({"answer": "120"})
|
110
108
|
{'answer': 120, 'comment': None, 'generated_tokens': None}
|
111
|
-
>>> rv.validate({"answer": "poo"})
|
112
|
-
Traceback (most recent call last):
|
113
|
-
...
|
114
|
-
edsl.questions.exceptions.QuestionAnswerValidationError:...
|
115
109
|
"""
|
116
110
|
proposed_edsl_answer_dict = self._preprocess(raw_edsl_answer_dict)
|
117
111
|
try:
|
edsl/results/__init__.py
CHANGED