edsl 0.1.49__py3-none-any.whl → 0.1.51__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 +107 -0
- edsl/buckets/model_buckets.py +1 -2
- edsl/buckets/token_bucket.py +11 -6
- edsl/buckets/token_bucket_api.py +27 -12
- edsl/buckets/token_bucket_client.py +9 -7
- edsl/caching/cache.py +12 -4
- edsl/caching/cache_entry.py +10 -9
- edsl/caching/exceptions.py +113 -7
- edsl/caching/remote_cache_sync.py +6 -7
- edsl/caching/sql_dict.py +20 -14
- edsl/cli.py +43 -0
- edsl/config/__init__.py +1 -1
- edsl/config/config_class.py +32 -6
- edsl/conversation/Conversation.py +8 -4
- edsl/conversation/car_buying.py +1 -3
- edsl/conversation/exceptions.py +58 -0
- edsl/conversation/mug_negotiation.py +2 -8
- edsl/coop/__init__.py +28 -6
- edsl/coop/coop.py +120 -29
- edsl/coop/coop_functions.py +1 -1
- edsl/coop/ep_key_handling.py +1 -1
- edsl/coop/exceptions.py +188 -9
- edsl/coop/price_fetcher.py +5 -8
- edsl/coop/utils.py +4 -6
- edsl/dataset/__init__.py +5 -4
- edsl/dataset/dataset.py +177 -86
- edsl/dataset/dataset_operations_mixin.py +98 -76
- edsl/dataset/dataset_tree.py +11 -7
- edsl/dataset/display/table_display.py +0 -2
- edsl/dataset/display/table_renderers.py +6 -4
- 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 +5 -6
- edsl/inference_services/data_structures.py +10 -7
- 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 +4 -3
- 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 +7 -3
- 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 +7 -5
- edsl/instructions/exceptions.py +61 -0
- edsl/instructions/instruction.py +6 -2
- edsl/instructions/instruction_collection.py +6 -4
- edsl/instructions/instruction_handler.py +12 -15
- edsl/interviews/ReportErrors.py +0 -3
- edsl/interviews/__init__.py +9 -2
- edsl/interviews/answering_function.py +11 -13
- edsl/interviews/exception_tracking.py +15 -8
- edsl/interviews/exceptions.py +79 -0
- edsl/interviews/interview.py +33 -30
- edsl/interviews/interview_status_dictionary.py +4 -2
- edsl/interviews/interview_status_log.py +2 -1
- edsl/interviews/interview_task_manager.py +5 -5
- edsl/interviews/request_token_estimator.py +5 -2
- edsl/interviews/statistics.py +3 -4
- edsl/invigilators/__init__.py +7 -1
- edsl/invigilators/exceptions.py +79 -0
- edsl/invigilators/invigilator_base.py +0 -1
- edsl/invigilators/invigilators.py +9 -13
- 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 +42 -5
- edsl/jobs/async_interview_runner.py +25 -23
- edsl/jobs/check_survey_scenario_compatibility.py +11 -10
- edsl/jobs/data_structures.py +8 -5
- edsl/jobs/exceptions.py +177 -8
- edsl/jobs/fetch_invigilator.py +1 -1
- edsl/jobs/jobs.py +74 -69
- edsl/jobs/jobs_checks.py +6 -7
- edsl/jobs/jobs_component_constructor.py +4 -4
- edsl/jobs/jobs_pricing_estimation.py +4 -3
- edsl/jobs/jobs_remote_inference_logger.py +5 -4
- edsl/jobs/jobs_runner_asyncio.py +3 -4
- edsl/jobs/jobs_runner_status.py +8 -9
- edsl/jobs/remote_inference.py +27 -24
- edsl/jobs/results_exceptions_handler.py +10 -7
- 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 +9 -8
- 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/__init__.py +24 -1
- edsl/notebooks/exceptions.py +82 -0
- edsl/notebooks/notebook.py +7 -3
- edsl/notebooks/notebook_to_latex.py +1 -2
- 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 +24 -1
- edsl/prompts/exceptions.py +107 -5
- edsl/prompts/prompt.py +15 -7
- edsl/questions/HTMLQuestion.py +5 -11
- edsl/questions/Quick.py +0 -1
- edsl/questions/__init__.py +6 -4
- edsl/questions/answer_validator_mixin.py +318 -323
- edsl/questions/compose_questions.py +3 -3
- edsl/questions/descriptors.py +11 -50
- 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 +46 -19
- edsl/questions/question_base_gen_mixin.py +2 -2
- edsl/questions/question_base_prompts_mixin.py +13 -7
- edsl/questions/question_budget.py +503 -98
- edsl/questions/question_check_box.py +660 -160
- edsl/questions/question_dict.py +345 -194
- edsl/questions/question_extract.py +401 -61
- edsl/questions/question_free_text.py +80 -14
- edsl/questions/question_functional.py +119 -9
- edsl/questions/{derived/question_likert_five.py → question_likert_five.py} +2 -2
- edsl/questions/{derived/question_linear_scale.py → question_linear_scale.py} +3 -4
- edsl/questions/question_list.py +275 -28
- edsl/questions/question_matrix.py +643 -96
- edsl/questions/question_multiple_choice.py +219 -51
- edsl/questions/question_numerical.py +361 -32
- edsl/questions/question_rank.py +401 -124
- edsl/questions/question_registry.py +7 -5
- edsl/questions/{derived/question_top_k.py → question_top_k.py} +3 -3
- edsl/questions/{derived/question_yes_no.py → question_yes_no.py} +3 -4
- edsl/questions/register_questions_meta.py +2 -2
- edsl/questions/response_validator_abc.py +13 -15
- edsl/questions/response_validator_factory.py +10 -12
- 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 +1 -2
- edsl/results/result.py +11 -9
- edsl/results/results.py +480 -321
- 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 +1 -3
- edsl/scenarios/scenario_list.py +179 -27
- 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_css.py +3 -3
- edsl/surveys/survey_export.py +6 -3
- edsl/surveys/survey_flow_visualization.py +10 -1
- edsl/surveys/survey_simulator.py +2 -1
- edsl/tasks/__init__.py +23 -1
- edsl/tasks/exceptions.py +72 -0
- edsl/tasks/question_task_creator.py +3 -3
- edsl/tasks/task_creators.py +1 -3
- edsl/tasks/task_history.py +8 -10
- edsl/tasks/task_status_log.py +1 -2
- edsl/tokens/__init__.py +29 -1
- edsl/tokens/exceptions.py +37 -0
- edsl/tokens/interview_token_usage.py +3 -2
- edsl/tokens/token_usage.py +4 -3
- 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.51.dist-info}/METADATA +32 -2
- edsl-0.1.51.dist-info/RECORD +365 -0
- edsl-0.1.51.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/questions/derived/__init__.py +0 -0
- 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.51.dist-info}/LICENSE +0 -0
- {edsl-0.1.49.dist-info → edsl-0.1.51.dist-info}/WHEEL +0 -0
@@ -1,11 +1,10 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
-
import
|
3
|
-
from typing import Any, Optional, Union, TYPE_CHECKING
|
2
|
+
from typing import Any, Optional, TYPE_CHECKING
|
4
3
|
|
4
|
+
import random
|
5
5
|
from jinja2 import Template
|
6
|
-
from pydantic import
|
7
|
-
from
|
8
|
-
from typing import List, Literal, Optional, Annotated
|
6
|
+
from pydantic import BaseModel, Field, model_validator, ValidationError
|
7
|
+
from typing import List, Literal, Annotated
|
9
8
|
|
10
9
|
from .exceptions import QuestionAnswerValidationError
|
11
10
|
from ..scenarios import Scenario
|
@@ -19,9 +18,45 @@ from .decorators import inject_exception
|
|
19
18
|
from .response_validator_abc import ResponseValidatorABC
|
20
19
|
|
21
20
|
if TYPE_CHECKING:
|
22
|
-
|
23
|
-
|
24
|
-
|
21
|
+
pass
|
22
|
+
|
23
|
+
|
24
|
+
class CheckboxResponse(BaseModel):
|
25
|
+
"""
|
26
|
+
Base Pydantic model for validating checkbox responses.
|
27
|
+
|
28
|
+
This model defines the structure and validation rules for responses to
|
29
|
+
checkbox questions, ensuring that selected options are properly formatted
|
30
|
+
as a list of choices.
|
31
|
+
|
32
|
+
Attributes:
|
33
|
+
answer: List of selected choices
|
34
|
+
comment: Optional comment provided with the answer
|
35
|
+
generated_tokens: Optional raw LLM output for token tracking
|
36
|
+
|
37
|
+
Examples:
|
38
|
+
>>> # Valid response with list of options
|
39
|
+
>>> response = CheckboxResponse(answer=[0, 1])
|
40
|
+
>>> response.answer
|
41
|
+
[0, 1]
|
42
|
+
|
43
|
+
>>> # Valid response with comment
|
44
|
+
>>> response = CheckboxResponse(answer=[1], comment="This is my choice")
|
45
|
+
>>> response.answer
|
46
|
+
[1]
|
47
|
+
>>> response.comment
|
48
|
+
'This is my choice'
|
49
|
+
|
50
|
+
>>> # Invalid non-list answer
|
51
|
+
>>> try:
|
52
|
+
... CheckboxResponse(answer=1)
|
53
|
+
... except Exception as e:
|
54
|
+
... print("Validation error occurred")
|
55
|
+
Validation error occurred
|
56
|
+
"""
|
57
|
+
answer: List[Any]
|
58
|
+
comment: Optional[str] = None
|
59
|
+
generated_tokens: Optional[Any] = None
|
25
60
|
|
26
61
|
|
27
62
|
def create_checkbox_response_model(
|
@@ -32,40 +67,218 @@ def create_checkbox_response_model(
|
|
32
67
|
):
|
33
68
|
"""
|
34
69
|
Dynamically create a CheckboxResponse model with a predefined list of choices.
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
70
|
+
|
71
|
+
This function creates a customized Pydantic model for checkbox questions that
|
72
|
+
validates both the format of the response and any constraints on selection count.
|
73
|
+
|
74
|
+
Args:
|
75
|
+
choices: A list of allowed values for the answer field
|
76
|
+
min_selections: Optional minimum number of selections required
|
77
|
+
max_selections: Optional maximum number of selections allowed
|
78
|
+
permissive: If True, constraints are not enforced
|
79
|
+
|
80
|
+
Returns:
|
81
|
+
A new Pydantic model class with appropriate validation
|
82
|
+
|
83
|
+
Examples:
|
84
|
+
>>> # Create model with constraints
|
85
|
+
>>> choices = [0, 1, 2, 3]
|
86
|
+
>>> ConstrainedModel = create_checkbox_response_model(
|
87
|
+
... choices=choices,
|
88
|
+
... min_selections=1,
|
89
|
+
... max_selections=2
|
90
|
+
... )
|
91
|
+
|
92
|
+
>>> # Valid response within constraints
|
93
|
+
>>> response = ConstrainedModel(answer=[0, 1])
|
94
|
+
>>> response.answer
|
95
|
+
[0, 1]
|
96
|
+
|
97
|
+
>>> # Too few selections fails validation
|
98
|
+
>>> try:
|
99
|
+
... ConstrainedModel(answer=[])
|
100
|
+
... except Exception as e:
|
101
|
+
... "at least 1" in str(e)
|
102
|
+
True
|
103
|
+
|
104
|
+
>>> # Too many selections fails validation
|
105
|
+
>>> try:
|
106
|
+
... ConstrainedModel(answer=[0, 1, 2])
|
107
|
+
... except Exception as e:
|
108
|
+
... "at most 2" in str(e)
|
109
|
+
True
|
110
|
+
|
111
|
+
>>> # Invalid choice fails validation
|
112
|
+
>>> try:
|
113
|
+
... ConstrainedModel(answer=[4])
|
114
|
+
... except Exception as e:
|
115
|
+
... any(x in str(e) for x in ["Invalid choice", "not a valid enumeration member", "validation error"])
|
116
|
+
True
|
117
|
+
|
118
|
+
>>> # Permissive model ignores constraints
|
119
|
+
>>> PermissiveModel = create_checkbox_response_model(
|
120
|
+
... choices=choices,
|
121
|
+
... min_selections=1,
|
122
|
+
... max_selections=2,
|
123
|
+
... permissive=True
|
124
|
+
... )
|
125
|
+
>>> response = PermissiveModel(answer=[0, 1, 2])
|
126
|
+
>>> len(response.answer)
|
127
|
+
3
|
39
128
|
"""
|
40
129
|
# Convert the choices list to a tuple for use with Literal
|
41
130
|
choice_tuple = tuple(choices)
|
42
131
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
132
|
+
if permissive:
|
133
|
+
# For permissive mode, we still validate the choice values but ignore count constraints
|
134
|
+
class PermissiveCheckboxResponse(CheckboxResponse):
|
135
|
+
"""Checkbox response model with choices validation but no count constraints."""
|
136
|
+
|
137
|
+
answer: Annotated[
|
138
|
+
List[Literal[choice_tuple]],
|
139
|
+
Field(description="List of selected choices"),
|
140
|
+
]
|
141
|
+
|
142
|
+
@model_validator(mode='after')
|
143
|
+
def validate_choices(self):
|
144
|
+
"""Validate that each selected choice is valid."""
|
145
|
+
for choice in self.answer:
|
146
|
+
if choice not in choices:
|
147
|
+
validation_error = ValidationError.from_exception_data(
|
148
|
+
title='CheckboxResponse',
|
149
|
+
line_errors=[{
|
150
|
+
'type': 'value_error',
|
151
|
+
'loc': ('answer',),
|
152
|
+
'msg': f'Invalid choice: {choice}. Must be one of: {choices}',
|
153
|
+
'input': choice,
|
154
|
+
'ctx': {'error': 'Invalid choice'}
|
155
|
+
}]
|
156
|
+
)
|
157
|
+
raise QuestionAnswerValidationError(
|
158
|
+
message=f"Invalid choice: {choice}. Must be one of: {choices}",
|
159
|
+
data=self.model_dump(),
|
160
|
+
model=self.__class__,
|
161
|
+
pydantic_error=validation_error
|
162
|
+
)
|
163
|
+
return self
|
164
|
+
|
165
|
+
return PermissiveCheckboxResponse
|
166
|
+
else:
|
167
|
+
# For non-permissive mode, enforce both choice values and count constraints
|
168
|
+
class ConstrainedCheckboxResponse(CheckboxResponse):
|
169
|
+
"""Checkbox response model with both choice and count constraints."""
|
170
|
+
|
171
|
+
answer: Annotated[
|
172
|
+
List[Literal[choice_tuple]],
|
173
|
+
Field(description="List of selected choices"),
|
174
|
+
]
|
175
|
+
|
176
|
+
@model_validator(mode='after')
|
177
|
+
def validate_selection_count(self):
|
178
|
+
"""Validate that the number of selections meets constraints."""
|
179
|
+
if min_selections is not None and len(self.answer) < min_selections:
|
180
|
+
validation_error = ValidationError.from_exception_data(
|
181
|
+
title='CheckboxResponse',
|
182
|
+
line_errors=[{
|
183
|
+
'type': 'value_error',
|
184
|
+
'loc': ('answer',),
|
185
|
+
'msg': f'Must select at least {min_selections} option(s)',
|
186
|
+
'input': self.answer,
|
187
|
+
'ctx': {'error': 'Too few selections'}
|
188
|
+
}]
|
189
|
+
)
|
190
|
+
raise QuestionAnswerValidationError(
|
191
|
+
message=f"Must select at least {min_selections} option(s), got {len(self.answer)}",
|
192
|
+
data=self.model_dump(),
|
193
|
+
model=self.__class__,
|
194
|
+
pydantic_error=validation_error
|
195
|
+
)
|
196
|
+
|
197
|
+
if max_selections is not None and len(self.answer) > max_selections:
|
198
|
+
validation_error = ValidationError.from_exception_data(
|
199
|
+
title='CheckboxResponse',
|
200
|
+
line_errors=[{
|
201
|
+
'type': 'value_error',
|
202
|
+
'loc': ('answer',),
|
203
|
+
'msg': f'Must select at most {max_selections} option(s)',
|
204
|
+
'input': self.answer,
|
205
|
+
'ctx': {'error': 'Too many selections'}
|
206
|
+
}]
|
207
|
+
)
|
208
|
+
raise QuestionAnswerValidationError(
|
209
|
+
message=f"Must select at most {max_selections} option(s), got {len(self.answer)}",
|
210
|
+
data=self.model_dump(),
|
211
|
+
model=self.__class__,
|
212
|
+
pydantic_error=validation_error
|
213
|
+
)
|
214
|
+
|
215
|
+
# Also validate that each choice is valid
|
216
|
+
for choice in self.answer:
|
217
|
+
if choice not in choices:
|
218
|
+
validation_error = ValidationError.from_exception_data(
|
219
|
+
title='CheckboxResponse',
|
220
|
+
line_errors=[{
|
221
|
+
'type': 'value_error',
|
222
|
+
'loc': ('answer',),
|
223
|
+
'msg': f'Invalid choice: {choice}. Must be one of: {choices}',
|
224
|
+
'input': choice,
|
225
|
+
'ctx': {'error': 'Invalid choice'}
|
226
|
+
}]
|
227
|
+
)
|
228
|
+
raise QuestionAnswerValidationError(
|
229
|
+
message=f"Invalid choice: {choice}. Must be one of: {choices}",
|
230
|
+
data=self.model_dump(),
|
231
|
+
model=self.__class__,
|
232
|
+
pydantic_error=validation_error
|
233
|
+
)
|
234
|
+
|
235
|
+
return self
|
236
|
+
|
237
|
+
return ConstrainedCheckboxResponse
|
66
238
|
|
67
239
|
|
68
240
|
class CheckBoxResponseValidator(ResponseValidatorABC):
|
241
|
+
"""
|
242
|
+
Validator for checkbox question responses.
|
243
|
+
|
244
|
+
This class implements the validation and fixing logic for checkbox responses.
|
245
|
+
It ensures that responses contain valid selections from the available options
|
246
|
+
and that the number of selections meets any constraints.
|
247
|
+
|
248
|
+
Attributes:
|
249
|
+
required_params: List of required parameters for validation.
|
250
|
+
valid_examples: Examples of valid responses for testing.
|
251
|
+
invalid_examples: Examples of invalid responses for testing.
|
252
|
+
|
253
|
+
Examples:
|
254
|
+
>>> from edsl import QuestionCheckBox
|
255
|
+
>>> q = QuestionCheckBox.example()
|
256
|
+
>>> validator = q.response_validator
|
257
|
+
|
258
|
+
>>> # Fix string to list
|
259
|
+
>>> response = {"answer": 1}
|
260
|
+
>>> fixed = validator.fix(response)
|
261
|
+
>>> isinstance(fixed["answer"], list)
|
262
|
+
True
|
263
|
+
|
264
|
+
>>> # Extract selections from text
|
265
|
+
>>> response = {"generated_tokens": "I choose options 0 and 2"}
|
266
|
+
>>> fixed = validator.fix(response)
|
267
|
+
>>> sorted(fixed["answer"])
|
268
|
+
[0, 2]
|
269
|
+
|
270
|
+
>>> # Fix comma-separated list
|
271
|
+
>>> response = {"generated_tokens": "0, 1, 3"}
|
272
|
+
>>> fixed = validator.fix(response)
|
273
|
+
>>> sorted(fixed["answer"])
|
274
|
+
[0, 1, 3]
|
275
|
+
|
276
|
+
>>> # Preserve comments when fixing
|
277
|
+
>>> response = {"answer": 1, "comment": "My explanation"}
|
278
|
+
>>> fixed = validator.fix(response)
|
279
|
+
>>> "comment" in fixed and fixed["comment"] == "My explanation"
|
280
|
+
True
|
281
|
+
"""
|
69
282
|
required_params = [
|
70
283
|
"question_options",
|
71
284
|
"min_selections",
|
@@ -82,12 +295,12 @@ class CheckBoxResponseValidator(ResponseValidatorABC):
|
|
82
295
|
(
|
83
296
|
{"answer": [-1]},
|
84
297
|
{"question_options": ["Good", "Great", "OK", "Bad"]},
|
85
|
-
"
|
298
|
+
"Invalid choice",
|
86
299
|
),
|
87
300
|
(
|
88
301
|
{"answer": 1},
|
89
302
|
{"question_options": ["Good", "Great", "OK", "Bad"]},
|
90
|
-
"
|
303
|
+
"value is not a valid list",
|
91
304
|
),
|
92
305
|
(
|
93
306
|
{"answer": [1, 2, 3, 4]},
|
@@ -96,84 +309,217 @@ class CheckBoxResponseValidator(ResponseValidatorABC):
|
|
96
309
|
"min_selections": 1,
|
97
310
|
"max_selections": 2,
|
98
311
|
},
|
99
|
-
"
|
312
|
+
"Must select at most 2",
|
100
313
|
),
|
101
314
|
]
|
102
315
|
|
103
316
|
def fix(self, response, verbose=False):
|
317
|
+
"""
|
318
|
+
Fix common issues in checkbox responses.
|
319
|
+
|
320
|
+
This method attempts to extract valid selections from responses with
|
321
|
+
format issues. It can handle:
|
322
|
+
1. Single values that should be lists
|
323
|
+
2. Comma-separated strings in answer field or generated_tokens
|
324
|
+
3. Finding option indices mentioned in text
|
325
|
+
|
326
|
+
Args:
|
327
|
+
response: The response dictionary to fix
|
328
|
+
verbose: If True, print information about the fixing process
|
329
|
+
|
330
|
+
Returns:
|
331
|
+
A fixed version of the response dictionary with a valid list of selections
|
332
|
+
|
333
|
+
Notes:
|
334
|
+
- First tries to convert to a list if the answer is not already a list
|
335
|
+
- Then tries to parse comma-separated values from answer or generated_tokens
|
336
|
+
- Finally tries to find option indices mentioned in the text
|
337
|
+
- Preserves any comment in the original response
|
338
|
+
"""
|
104
339
|
if verbose:
|
105
340
|
print("Invalid response of QuestionCheckBox was: ", response)
|
106
|
-
|
107
|
-
if
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
341
|
+
|
342
|
+
# Check if answer exists and is a comma-separated string (common LLM output format)
|
343
|
+
if "answer" in response and isinstance(response["answer"], str) and "," in response["answer"]:
|
344
|
+
if verbose:
|
345
|
+
print(f"Parsing comma-separated answer string: {response['answer']}")
|
346
|
+
|
347
|
+
# Split by commas and strip whitespace
|
348
|
+
proposed_list = response["answer"].split(",")
|
349
|
+
proposed_list = [item.strip() for item in proposed_list]
|
350
|
+
|
351
|
+
# Try to convert to integers if use_code is True
|
352
|
+
if self.use_code:
|
353
|
+
try:
|
354
|
+
proposed_list = [int(i) for i in proposed_list]
|
355
|
+
except ValueError:
|
356
|
+
# If we can't convert to integers, try to match values to indices
|
357
|
+
if verbose:
|
358
|
+
print("Could not convert comma-separated values to integers, trying to match options")
|
359
|
+
|
360
|
+
# Try to match option text values to their indices
|
361
|
+
index_map = {}
|
362
|
+
for i, option in enumerate(self.question_options):
|
363
|
+
index_map[option.lower().strip()] = i
|
364
|
+
|
365
|
+
converted_list = []
|
366
|
+
for item in proposed_list:
|
367
|
+
item_lower = item.lower().strip()
|
368
|
+
if item_lower in index_map:
|
369
|
+
converted_list.append(index_map[item_lower])
|
370
|
+
|
371
|
+
if converted_list:
|
372
|
+
proposed_list = converted_list
|
373
|
+
|
374
|
+
if verbose:
|
375
|
+
print("Proposed solution from comma separation is: ", proposed_list)
|
376
|
+
|
132
377
|
proposed_data = {
|
133
378
|
"answer": proposed_list,
|
134
|
-
"
|
379
|
+
"comment": response.get("comment"),
|
380
|
+
"generated_tokens": response.get("generated_tokens"),
|
135
381
|
}
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
print("Now seeing if responses show up in the answer")
|
148
|
-
matches = []
|
149
|
-
for index, option in enumerate(self.question_options):
|
150
|
-
if self.use_code:
|
151
|
-
if str(index) in response_text:
|
152
|
-
matches.append(index)
|
153
|
-
else:
|
154
|
-
if option in response_text:
|
155
|
-
matches.append(index)
|
156
|
-
proposed_data = {
|
157
|
-
"answer": matches,
|
158
|
-
"comment": response.get("comment", None),
|
159
|
-
"generated_tokens": response.get("generated_tokens", None),
|
160
|
-
}
|
161
|
-
try:
|
162
|
-
self.response_model(**proposed_data)
|
163
|
-
return proposed_data
|
164
|
-
except Exception as e:
|
382
|
+
|
383
|
+
# Try validating with the proposed solution
|
384
|
+
try:
|
385
|
+
validated = self._base_validate(proposed_data)
|
386
|
+
return validated.model_dump()
|
387
|
+
except Exception as e:
|
388
|
+
if verbose:
|
389
|
+
print(f"Comma-separated solution invalid: {e}")
|
390
|
+
|
391
|
+
# If answer exists but is not a list, convert it to a list
|
392
|
+
elif "answer" in response and not isinstance(response["answer"], list):
|
165
393
|
if verbose:
|
166
|
-
print(f"
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
394
|
+
print(f"Converting non-list answer {response['answer']} to a list")
|
395
|
+
answer_value = response["answer"]
|
396
|
+
response = {**response, "answer": [answer_value]}
|
397
|
+
|
398
|
+
# Try validating the fixed response
|
399
|
+
try:
|
400
|
+
validated = self._base_validate(response)
|
401
|
+
return validated.model_dump()
|
402
|
+
except Exception:
|
403
|
+
if verbose:
|
404
|
+
print("Converting to list didn't fix the issue")
|
405
|
+
|
406
|
+
# Try parsing from generated_tokens if present
|
407
|
+
response_text = response.get("generated_tokens")
|
408
|
+
if response_text and isinstance(response_text, str):
|
409
|
+
# Try comma-separated list first
|
410
|
+
if "," in response_text:
|
411
|
+
proposed_list = response_text.split(",")
|
412
|
+
proposed_list = [item.strip() for item in proposed_list]
|
413
|
+
|
414
|
+
if self.use_code:
|
415
|
+
try:
|
416
|
+
proposed_list = [int(i) for i in proposed_list]
|
417
|
+
except ValueError:
|
418
|
+
# If we can't convert to integers, try to match values to indices
|
419
|
+
if verbose:
|
420
|
+
print("Could not convert comma-separated values to integers, trying to match options")
|
421
|
+
|
422
|
+
# Try to match option text values to their indices
|
423
|
+
index_map = {}
|
424
|
+
for i, option in enumerate(self.question_options):
|
425
|
+
index_map[option.lower().strip()] = i
|
426
|
+
|
427
|
+
converted_list = []
|
428
|
+
for item in proposed_list:
|
429
|
+
item_lower = item.lower().strip()
|
430
|
+
if item_lower in index_map:
|
431
|
+
converted_list.append(index_map[item_lower])
|
432
|
+
|
433
|
+
if converted_list:
|
434
|
+
proposed_list = converted_list
|
435
|
+
|
436
|
+
if verbose:
|
437
|
+
print("Proposed solution from comma separation is: ", proposed_list)
|
438
|
+
|
439
|
+
proposed_data = {
|
440
|
+
"answer": proposed_list,
|
441
|
+
"comment": response.get("comment"),
|
442
|
+
"generated_tokens": response.get("generated_tokens"),
|
443
|
+
}
|
444
|
+
|
445
|
+
# Try validating with the proposed solution
|
446
|
+
try:
|
447
|
+
validated = self._base_validate(proposed_data)
|
448
|
+
return validated.model_dump()
|
449
|
+
except Exception as e:
|
450
|
+
if verbose:
|
451
|
+
print(f"Comma-separated solution invalid: {e}")
|
452
|
+
|
453
|
+
# Try finding option indices mentioned in the text
|
454
|
+
matches = []
|
455
|
+
for index, option in enumerate(self.question_options):
|
456
|
+
if self.use_code:
|
457
|
+
if str(index) in response_text:
|
458
|
+
matches.append(index)
|
459
|
+
else:
|
460
|
+
if option in response_text:
|
461
|
+
matches.append(option)
|
462
|
+
|
463
|
+
if matches:
|
464
|
+
if verbose:
|
465
|
+
print(f"Found options mentioned in text: {matches}")
|
466
|
+
|
467
|
+
proposed_data = {
|
468
|
+
"answer": matches,
|
469
|
+
"comment": response.get("comment"),
|
470
|
+
"generated_tokens": response.get("generated_tokens"),
|
471
|
+
}
|
472
|
+
|
473
|
+
# Try validating with the proposed solution
|
474
|
+
try:
|
475
|
+
validated = self._base_validate(proposed_data)
|
476
|
+
return validated.model_dump()
|
477
|
+
except Exception as e:
|
478
|
+
if verbose:
|
479
|
+
print(f"Text matching solution invalid: {e}")
|
480
|
+
|
481
|
+
# If nothing worked, return the original response
|
482
|
+
return response
|
173
483
|
|
174
484
|
|
175
485
|
class QuestionCheckBox(QuestionBase):
|
176
|
-
"""
|
486
|
+
"""
|
487
|
+
A question that prompts the agent to select multiple options from a list.
|
488
|
+
|
489
|
+
QuestionCheckBox allows agents to select one or more items from a predefined
|
490
|
+
list of options. It's useful for "select all that apply" scenarios, multi-select
|
491
|
+
preferences, or any question where multiple valid selections are possible.
|
492
|
+
|
493
|
+
Attributes:
|
494
|
+
question_type (str): Identifier for this question type, set to "checkbox".
|
495
|
+
purpose (str): Brief description of when to use this question type.
|
496
|
+
question_options: List of available options to select from.
|
497
|
+
min_selections: Optional minimum number of selections required.
|
498
|
+
max_selections: Optional maximum number of selections allowed.
|
499
|
+
_response_model: Initially None, set by create_response_model().
|
500
|
+
response_validator_class: Class used to validate and fix responses.
|
501
|
+
|
502
|
+
Examples:
|
503
|
+
>>> # Basic creation works
|
504
|
+
>>> q = QuestionCheckBox.example()
|
505
|
+
>>> q.question_type
|
506
|
+
'checkbox'
|
507
|
+
|
508
|
+
>>> # Create preferences question with selection constraints
|
509
|
+
>>> q = QuestionCheckBox(
|
510
|
+
... question_name="favorite_fruits",
|
511
|
+
... question_text="Which fruits do you like?",
|
512
|
+
... question_options=["Apple", "Banana", "Cherry", "Durian", "Elderberry"],
|
513
|
+
... min_selections=1,
|
514
|
+
... max_selections=3
|
515
|
+
... )
|
516
|
+
>>> q.question_options
|
517
|
+
['Apple', 'Banana', 'Cherry', 'Durian', 'Elderberry']
|
518
|
+
>>> q.min_selections
|
519
|
+
1
|
520
|
+
>>> q.max_selections
|
521
|
+
3
|
522
|
+
"""
|
177
523
|
|
178
524
|
question_type = "checkbox"
|
179
525
|
purpose = "When options are known and limited"
|
@@ -197,13 +543,41 @@ class QuestionCheckBox(QuestionBase):
|
|
197
543
|
answering_instructions: Optional[str] = None,
|
198
544
|
permissive: bool = False,
|
199
545
|
):
|
200
|
-
"""
|
201
|
-
|
202
|
-
|
203
|
-
:
|
204
|
-
|
205
|
-
|
206
|
-
|
546
|
+
"""
|
547
|
+
Initialize a new checkbox question.
|
548
|
+
|
549
|
+
Args:
|
550
|
+
question_name: Identifier for the question, used in results and templates.
|
551
|
+
Must be a valid Python variable name.
|
552
|
+
question_text: The actual text of the question to be asked.
|
553
|
+
question_options: List of options the agent can select from.
|
554
|
+
min_selections: Optional minimum number of options that must be selected.
|
555
|
+
max_selections: Optional maximum number of options that can be selected.
|
556
|
+
include_comment: Whether to allow comments with the answer.
|
557
|
+
use_code: If True, use indices (0,1,2...) instead of option text values.
|
558
|
+
question_presentation: Optional custom presentation template.
|
559
|
+
answering_instructions: Optional additional instructions.
|
560
|
+
permissive: If True, ignore selection count constraints during validation.
|
561
|
+
|
562
|
+
Examples:
|
563
|
+
>>> q = QuestionCheckBox(
|
564
|
+
... question_name="symptoms",
|
565
|
+
... question_text="Select all symptoms you are experiencing:",
|
566
|
+
... question_options=["Fever", "Cough", "Headache", "Fatigue"],
|
567
|
+
... min_selections=1
|
568
|
+
... )
|
569
|
+
>>> q.question_name
|
570
|
+
'symptoms'
|
571
|
+
|
572
|
+
>>> # Question with both min and max
|
573
|
+
>>> q = QuestionCheckBox(
|
574
|
+
... question_name="pizza_toppings",
|
575
|
+
... question_text="Choose 2-4 toppings for your pizza:",
|
576
|
+
... question_options=["Cheese", "Pepperoni", "Mushroom", "Onion",
|
577
|
+
... "Sausage", "Bacon", "Pineapple"],
|
578
|
+
... min_selections=2,
|
579
|
+
... max_selections=4
|
580
|
+
... )
|
207
581
|
"""
|
208
582
|
self.question_name = question_name
|
209
583
|
self.question_text = question_text
|
@@ -219,18 +593,35 @@ class QuestionCheckBox(QuestionBase):
|
|
219
593
|
self.answering_instructions = answering_instructions
|
220
594
|
|
221
595
|
def create_response_model(self):
|
596
|
+
"""
|
597
|
+
Create a response model with the appropriate constraints.
|
598
|
+
|
599
|
+
This method creates a Pydantic model customized with the options and
|
600
|
+
selection count constraints specified for this question instance.
|
601
|
+
|
602
|
+
Returns:
|
603
|
+
A Pydantic model class tailored to this question's constraints.
|
604
|
+
|
605
|
+
Examples:
|
606
|
+
>>> q = QuestionCheckBox.example()
|
607
|
+
>>> model = q.create_response_model()
|
608
|
+
>>> model(answer=[0, 2]) # Select first and third options
|
609
|
+
ConstrainedCheckboxResponse(answer=[0, 2], comment=None, generated_tokens=None)
|
610
|
+
"""
|
222
611
|
if not self._use_code:
|
612
|
+
# Use option text values as valid choices
|
223
613
|
return create_checkbox_response_model(
|
224
614
|
self.question_options,
|
225
615
|
min_selections=self.min_selections,
|
226
|
-
max_selections=self.max_selections,
|
616
|
+
max_selections=self.max_selections,
|
227
617
|
permissive=self.permissive,
|
228
618
|
)
|
229
619
|
else:
|
620
|
+
# Use option indices (0, 1, 2...) as valid choices
|
230
621
|
return create_checkbox_response_model(
|
231
622
|
list(range(len(self.question_options))),
|
232
623
|
min_selections=self.min_selections,
|
233
|
-
max_selections=self.max_selections,
|
624
|
+
max_selections=self.max_selections,
|
234
625
|
permissive=self.permissive,
|
235
626
|
)
|
236
627
|
|
@@ -238,10 +629,27 @@ class QuestionCheckBox(QuestionBase):
|
|
238
629
|
self, answer_codes, scenario: "Scenario" = None
|
239
630
|
):
|
240
631
|
"""
|
241
|
-
Translate the answer
|
242
|
-
|
243
|
-
For
|
244
|
-
|
632
|
+
Translate the answer codes to the actual answer text.
|
633
|
+
|
634
|
+
For checkbox questions with use_code=True, the agent responds with
|
635
|
+
option indices (e.g., [0, 1]) which need to be translated to their
|
636
|
+
corresponding option text values (e.g., ["Option A", "Option B"]).
|
637
|
+
|
638
|
+
Args:
|
639
|
+
answer_codes: List of selected option indices or values
|
640
|
+
scenario: Optional scenario with variables for template rendering
|
641
|
+
|
642
|
+
Returns:
|
643
|
+
List of selected option texts
|
644
|
+
|
645
|
+
Examples:
|
646
|
+
>>> q = QuestionCheckBox(
|
647
|
+
... question_name="example",
|
648
|
+
... question_text="Select options:",
|
649
|
+
... question_options=["A", "B", "C"]
|
650
|
+
... )
|
651
|
+
>>> q._translate_answer_code_to_answer([0, 2])
|
652
|
+
['A', 'C']
|
245
653
|
"""
|
246
654
|
scenario = scenario or Scenario()
|
247
655
|
translated_options = [
|
@@ -255,38 +663,73 @@ class QuestionCheckBox(QuestionBase):
|
|
255
663
|
translated_codes.append(answer_code)
|
256
664
|
return translated_codes
|
257
665
|
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
666
|
+
def _simulate_answer(self, human_readable=True):
|
667
|
+
"""
|
668
|
+
Simulate a valid answer for debugging purposes.
|
669
|
+
|
670
|
+
This method generates a random valid answer for the checkbox question,
|
671
|
+
useful for testing and demonstrations.
|
672
|
+
|
673
|
+
Args:
|
674
|
+
human_readable: If True, return option text values; if False, return indices
|
675
|
+
|
676
|
+
Returns:
|
677
|
+
A dictionary with a valid random answer
|
678
|
+
|
679
|
+
Examples:
|
680
|
+
>>> q = QuestionCheckBox.example()
|
681
|
+
>>> answer = q._simulate_answer(human_readable=False)
|
682
|
+
>>> len(answer["answer"]) >= q.min_selections
|
683
|
+
True
|
684
|
+
>>> len(answer["answer"]) <= q.max_selections
|
685
|
+
True
|
686
|
+
"""
|
687
|
+
from edsl.utilities.utilities import random_string
|
688
|
+
|
689
|
+
min_sel = self.min_selections or 1
|
690
|
+
max_sel = self.max_selections or len(self.question_options)
|
691
|
+
# Ensure we don't try to select more options than available
|
692
|
+
max_sel = min(max_sel, len(self.question_options))
|
693
|
+
min_sel = min(min_sel, max_sel)
|
694
|
+
|
695
|
+
num_selections = random.randint(min_sel, max_sel)
|
696
|
+
|
697
|
+
if human_readable:
|
698
|
+
# Select a random number of options from self.question_options
|
699
|
+
selected_options = random.sample(self.question_options, num_selections)
|
700
|
+
answer = {
|
701
|
+
"answer": selected_options,
|
702
|
+
"comment": random_string(),
|
703
|
+
}
|
704
|
+
else:
|
705
|
+
# Select a random number of indices from the range of self.question_options
|
706
|
+
selected_indices = random.sample(
|
707
|
+
range(len(self.question_options)), num_selections
|
708
|
+
)
|
709
|
+
answer = {
|
710
|
+
"answer": selected_indices,
|
711
|
+
"comment": random_string(),
|
712
|
+
}
|
713
|
+
return answer
|
282
714
|
|
283
715
|
@property
|
284
716
|
def question_html_content(self) -> str:
|
717
|
+
"""
|
718
|
+
Generate HTML content for rendering the question in web interfaces.
|
719
|
+
|
720
|
+
This property generates HTML markup for the question when it needs to be
|
721
|
+
displayed in web interfaces or HTML contexts. For a checkbox question,
|
722
|
+
this is a set of checkbox input elements, one for each option.
|
723
|
+
|
724
|
+
Returns:
|
725
|
+
str: HTML markup for rendering the question.
|
726
|
+
"""
|
285
727
|
instructions = ""
|
286
728
|
if self.min_selections is not None:
|
287
729
|
instructions += f"Select at least {self.min_selections} option(s). "
|
288
730
|
if self.max_selections is not None:
|
289
731
|
instructions += f"Select at most {self.max_selections} option(s)."
|
732
|
+
|
290
733
|
question_html_content = Template(
|
291
734
|
"""
|
292
735
|
<p>{{ instructions }}</p>
|
@@ -310,7 +753,30 @@ class QuestionCheckBox(QuestionBase):
|
|
310
753
|
@classmethod
|
311
754
|
@inject_exception
|
312
755
|
def example(cls, include_comment=False, use_code=True) -> QuestionCheckBox:
|
313
|
-
"""
|
756
|
+
"""
|
757
|
+
Create an example instance of a checkbox question.
|
758
|
+
|
759
|
+
This class method creates a predefined example of a checkbox question
|
760
|
+
for demonstration, testing, and documentation purposes.
|
761
|
+
|
762
|
+
Args:
|
763
|
+
include_comment: Whether to include a comment field with the answer.
|
764
|
+
use_code: Whether to use indices (True) or values (False) for answer codes.
|
765
|
+
|
766
|
+
Returns:
|
767
|
+
QuestionCheckBox: An example checkbox question.
|
768
|
+
|
769
|
+
Examples:
|
770
|
+
>>> q = QuestionCheckBox.example()
|
771
|
+
>>> q.question_name
|
772
|
+
'never_eat'
|
773
|
+
>>> len(q.question_options)
|
774
|
+
5
|
775
|
+
>>> q.min_selections
|
776
|
+
2
|
777
|
+
>>> q.max_selections
|
778
|
+
5
|
779
|
+
"""
|
314
780
|
return cls(
|
315
781
|
question_name="never_eat",
|
316
782
|
question_text="Which of the following foods would you eat if you had to?",
|
@@ -329,31 +795,65 @@ class QuestionCheckBox(QuestionBase):
|
|
329
795
|
|
330
796
|
|
331
797
|
def main():
|
332
|
-
"""
|
333
|
-
|
334
|
-
|
798
|
+
"""
|
799
|
+
Demonstrate the functionality of the QuestionCheckBox class.
|
800
|
+
|
801
|
+
This function creates an example checkbox question and demonstrates its
|
802
|
+
key features including validation, serialization, and answer simulation.
|
803
|
+
It's primarily intended for testing and development purposes.
|
804
|
+
|
805
|
+
Note:
|
806
|
+
This function will be executed when the module is run directly,
|
807
|
+
but not when imported.
|
808
|
+
"""
|
809
|
+
print("Creating a QuestionCheckBox example...")
|
335
810
|
q = QuestionCheckBox.example()
|
336
|
-
q.question_text
|
337
|
-
q.
|
338
|
-
q.
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
q.
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
811
|
+
print(f"Question text: {q.question_text}")
|
812
|
+
print(f"Question name: {q.question_name}")
|
813
|
+
print(f"Question options: {q.question_options}")
|
814
|
+
print(f"Min selections: {q.min_selections}")
|
815
|
+
print(f"Max selections: {q.max_selections}")
|
816
|
+
|
817
|
+
# Validate an answer
|
818
|
+
print("\nValidating an answer...")
|
819
|
+
valid_answer = {"answer": [1, 2], "comment": "I like these foods"}
|
820
|
+
validated = q._validate_answer(valid_answer)
|
821
|
+
print(f"Validated answer: {validated}")
|
822
|
+
|
823
|
+
# Translate answer codes
|
824
|
+
print("\nTranslating answer codes...")
|
825
|
+
translated = q._translate_answer_code_to_answer([1, 2])
|
826
|
+
print(f"Translated answer: {translated}")
|
827
|
+
|
828
|
+
# Simulate answers
|
829
|
+
print("\nSimulating answers...")
|
830
|
+
simulated_human = q._simulate_answer(human_readable=True)
|
831
|
+
print(f"Simulated human-readable answer: {simulated_human}")
|
832
|
+
|
833
|
+
simulated_codes = q._simulate_answer(human_readable=False)
|
834
|
+
print(f"Simulated code answer: {simulated_codes}")
|
835
|
+
|
836
|
+
# Validate simulated answer
|
837
|
+
validated_simulated = q._validate_answer(simulated_codes)
|
838
|
+
print(f"Validated simulated answer: {validated_simulated}")
|
839
|
+
|
840
|
+
# Serialization demonstration
|
841
|
+
print("\nTesting serialization...")
|
842
|
+
serialized = q.to_dict()
|
843
|
+
print(f"Serialized question (keys): {list(serialized.keys())}")
|
844
|
+
deserialized = QuestionBase.from_dict(serialized)
|
845
|
+
print(f"Deserialization successful: {deserialized.question_text == q.question_text}")
|
846
|
+
|
847
|
+
# Run doctests
|
848
|
+
print("\nRunning doctests...")
|
351
849
|
import doctest
|
352
|
-
|
353
850
|
doctest.testmod(optionflags=doctest.ELLIPSIS)
|
851
|
+
print("Doctests completed")
|
354
852
|
|
355
853
|
|
356
854
|
if __name__ == "__main__":
|
357
855
|
import doctest
|
358
|
-
|
359
856
|
doctest.testmod(optionflags=doctest.ELLIPSIS)
|
857
|
+
|
858
|
+
# Uncomment to run demonstration
|
859
|
+
# main()
|