edsl 0.1.39.dev3__py3-none-any.whl → 0.1.39.dev5__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 +413 -332
- edsl/BaseDiff.py +260 -260
- edsl/TemplateLoader.py +24 -24
- edsl/__init__.py +57 -49
- edsl/__version__.py +1 -1
- edsl/agents/Agent.py +1071 -867
- edsl/agents/AgentList.py +551 -413
- edsl/agents/Invigilator.py +284 -233
- edsl/agents/InvigilatorBase.py +257 -270
- edsl/agents/PromptConstructor.py +272 -354
- edsl/agents/QuestionInstructionPromptBuilder.py +128 -0
- edsl/agents/QuestionTemplateReplacementsBuilder.py +137 -0
- edsl/agents/__init__.py +2 -3
- edsl/agents/descriptors.py +99 -99
- edsl/agents/prompt_helpers.py +129 -129
- edsl/agents/question_option_processor.py +172 -0
- edsl/auto/AutoStudy.py +130 -117
- edsl/auto/StageBase.py +243 -230
- edsl/auto/StageGenerateSurvey.py +178 -178
- edsl/auto/StageLabelQuestions.py +125 -125
- edsl/auto/StagePersona.py +61 -61
- edsl/auto/StagePersonaDimensionValueRanges.py +88 -88
- edsl/auto/StagePersonaDimensionValues.py +74 -74
- edsl/auto/StagePersonaDimensions.py +69 -69
- edsl/auto/StageQuestions.py +74 -73
- edsl/auto/SurveyCreatorPipeline.py +21 -21
- edsl/auto/utilities.py +218 -224
- edsl/base/Base.py +279 -279
- edsl/config.py +177 -157
- edsl/conversation/Conversation.py +290 -290
- edsl/conversation/car_buying.py +59 -58
- edsl/conversation/chips.py +95 -95
- edsl/conversation/mug_negotiation.py +81 -81
- edsl/conversation/next_speaker_utilities.py +93 -93
- edsl/coop/CoopFunctionsMixin.py +15 -0
- edsl/coop/ExpectedParrotKeyHandler.py +125 -0
- edsl/coop/PriceFetcher.py +54 -54
- edsl/coop/__init__.py +2 -2
- edsl/coop/coop.py +1106 -1028
- edsl/coop/utils.py +131 -131
- edsl/data/Cache.py +573 -555
- edsl/data/CacheEntry.py +230 -233
- edsl/data/CacheHandler.py +168 -149
- edsl/data/RemoteCacheSync.py +186 -78
- edsl/data/SQLiteDict.py +292 -292
- edsl/data/__init__.py +5 -4
- edsl/data/orm.py +10 -10
- edsl/data_transfer_models.py +74 -73
- edsl/enums.py +202 -175
- edsl/exceptions/BaseException.py +21 -21
- edsl/exceptions/__init__.py +54 -54
- edsl/exceptions/agents.py +54 -42
- edsl/exceptions/cache.py +5 -5
- edsl/exceptions/configuration.py +16 -16
- edsl/exceptions/coop.py +10 -10
- edsl/exceptions/data.py +14 -14
- edsl/exceptions/general.py +34 -34
- edsl/exceptions/inference_services.py +5 -0
- edsl/exceptions/jobs.py +33 -33
- edsl/exceptions/language_models.py +63 -63
- edsl/exceptions/prompts.py +15 -15
- edsl/exceptions/questions.py +109 -91
- edsl/exceptions/results.py +29 -29
- edsl/exceptions/scenarios.py +29 -22
- edsl/exceptions/surveys.py +37 -37
- edsl/inference_services/AnthropicService.py +106 -87
- edsl/inference_services/AvailableModelCacheHandler.py +184 -0
- edsl/inference_services/AvailableModelFetcher.py +215 -0
- edsl/inference_services/AwsBedrock.py +118 -120
- edsl/inference_services/AzureAI.py +215 -217
- edsl/inference_services/DeepInfraService.py +18 -18
- edsl/inference_services/GoogleService.py +143 -148
- edsl/inference_services/GroqService.py +20 -20
- edsl/inference_services/InferenceServiceABC.py +80 -147
- edsl/inference_services/InferenceServicesCollection.py +138 -97
- edsl/inference_services/MistralAIService.py +120 -123
- edsl/inference_services/OllamaService.py +18 -18
- edsl/inference_services/OpenAIService.py +236 -224
- edsl/inference_services/PerplexityService.py +160 -163
- edsl/inference_services/ServiceAvailability.py +135 -0
- edsl/inference_services/TestService.py +90 -89
- edsl/inference_services/TogetherAIService.py +172 -170
- edsl/inference_services/data_structures.py +134 -0
- edsl/inference_services/models_available_cache.py +118 -118
- edsl/inference_services/rate_limits_cache.py +25 -25
- edsl/inference_services/registry.py +41 -41
- edsl/inference_services/write_available.py +10 -10
- edsl/jobs/AnswerQuestionFunctionConstructor.py +223 -0
- edsl/jobs/Answers.py +43 -56
- edsl/jobs/FetchInvigilator.py +47 -0
- edsl/jobs/InterviewTaskManager.py +98 -0
- edsl/jobs/InterviewsConstructor.py +50 -0
- edsl/jobs/Jobs.py +823 -898
- edsl/jobs/JobsChecks.py +172 -147
- edsl/jobs/JobsComponentConstructor.py +189 -0
- edsl/jobs/JobsPrompts.py +270 -268
- edsl/jobs/JobsRemoteInferenceHandler.py +311 -239
- edsl/jobs/JobsRemoteInferenceLogger.py +239 -0
- edsl/jobs/RequestTokenEstimator.py +30 -0
- edsl/jobs/__init__.py +1 -1
- edsl/jobs/async_interview_runner.py +138 -0
- edsl/jobs/buckets/BucketCollection.py +104 -63
- edsl/jobs/buckets/ModelBuckets.py +65 -65
- edsl/jobs/buckets/TokenBucket.py +283 -251
- edsl/jobs/buckets/TokenBucketAPI.py +211 -0
- edsl/jobs/buckets/TokenBucketClient.py +191 -0
- edsl/jobs/check_survey_scenario_compatibility.py +85 -0
- edsl/jobs/data_structures.py +120 -0
- edsl/jobs/decorators.py +35 -0
- edsl/jobs/interviews/Interview.py +396 -661
- edsl/jobs/interviews/InterviewExceptionCollection.py +99 -99
- edsl/jobs/interviews/InterviewExceptionEntry.py +186 -186
- edsl/jobs/interviews/InterviewStatistic.py +63 -63
- edsl/jobs/interviews/InterviewStatisticsCollection.py +25 -25
- edsl/jobs/interviews/InterviewStatusDictionary.py +78 -78
- edsl/jobs/interviews/InterviewStatusLog.py +92 -92
- edsl/jobs/interviews/ReportErrors.py +66 -66
- edsl/jobs/interviews/interview_status_enum.py +9 -9
- edsl/jobs/jobs_status_enums.py +9 -0
- edsl/jobs/loggers/HTMLTableJobLogger.py +304 -0
- edsl/jobs/results_exceptions_handler.py +98 -0
- edsl/jobs/runners/JobsRunnerAsyncio.py +151 -466
- edsl/jobs/runners/JobsRunnerStatus.py +297 -330
- edsl/jobs/tasks/QuestionTaskCreator.py +244 -242
- edsl/jobs/tasks/TaskCreators.py +64 -64
- edsl/jobs/tasks/TaskHistory.py +470 -450
- edsl/jobs/tasks/TaskStatusLog.py +23 -23
- edsl/jobs/tasks/task_status_enum.py +161 -163
- edsl/jobs/tokens/InterviewTokenUsage.py +27 -27
- edsl/jobs/tokens/TokenUsage.py +34 -34
- edsl/language_models/ComputeCost.py +63 -0
- edsl/language_models/LanguageModel.py +626 -668
- edsl/language_models/ModelList.py +164 -155
- edsl/language_models/PriceManager.py +127 -0
- edsl/language_models/RawResponseHandler.py +106 -0
- edsl/language_models/RegisterLanguageModelsMeta.py +184 -184
- edsl/language_models/ServiceDataSources.py +0 -0
- edsl/language_models/__init__.py +2 -3
- edsl/language_models/fake_openai_call.py +15 -15
- edsl/language_models/fake_openai_service.py +61 -61
- edsl/language_models/key_management/KeyLookup.py +63 -0
- edsl/language_models/key_management/KeyLookupBuilder.py +273 -0
- edsl/language_models/key_management/KeyLookupCollection.py +38 -0
- edsl/language_models/key_management/__init__.py +0 -0
- edsl/language_models/key_management/models.py +131 -0
- edsl/language_models/model.py +256 -0
- edsl/language_models/repair.py +156 -156
- edsl/language_models/utilities.py +65 -64
- edsl/notebooks/Notebook.py +263 -258
- edsl/notebooks/NotebookToLaTeX.py +142 -0
- edsl/notebooks/__init__.py +1 -1
- edsl/prompts/Prompt.py +352 -362
- edsl/prompts/__init__.py +2 -2
- edsl/questions/ExceptionExplainer.py +77 -0
- edsl/questions/HTMLQuestion.py +103 -0
- edsl/questions/QuestionBase.py +518 -664
- edsl/questions/QuestionBasePromptsMixin.py +221 -217
- edsl/questions/QuestionBudget.py +227 -227
- edsl/questions/QuestionCheckBox.py +359 -359
- edsl/questions/QuestionExtract.py +180 -182
- edsl/questions/QuestionFreeText.py +113 -114
- edsl/questions/QuestionFunctional.py +166 -166
- edsl/questions/QuestionList.py +223 -231
- edsl/questions/QuestionMatrix.py +265 -0
- edsl/questions/QuestionMultipleChoice.py +330 -286
- edsl/questions/QuestionNumerical.py +151 -153
- edsl/questions/QuestionRank.py +314 -324
- edsl/questions/Quick.py +41 -41
- edsl/questions/SimpleAskMixin.py +74 -73
- edsl/questions/__init__.py +27 -26
- edsl/questions/{AnswerValidatorMixin.py → answer_validator_mixin.py} +334 -289
- edsl/questions/compose_questions.py +98 -98
- edsl/questions/data_structures.py +20 -0
- edsl/questions/decorators.py +21 -21
- edsl/questions/derived/QuestionLikertFive.py +76 -76
- edsl/questions/derived/QuestionLinearScale.py +90 -87
- edsl/questions/derived/QuestionTopK.py +93 -93
- edsl/questions/derived/QuestionYesNo.py +82 -82
- edsl/questions/descriptors.py +427 -413
- edsl/questions/loop_processor.py +149 -0
- edsl/questions/prompt_templates/question_budget.jinja +13 -13
- edsl/questions/prompt_templates/question_checkbox.jinja +32 -32
- edsl/questions/prompt_templates/question_extract.jinja +11 -11
- edsl/questions/prompt_templates/question_free_text.jinja +3 -3
- edsl/questions/prompt_templates/question_linear_scale.jinja +11 -11
- edsl/questions/prompt_templates/question_list.jinja +17 -17
- edsl/questions/prompt_templates/question_multiple_choice.jinja +33 -33
- edsl/questions/prompt_templates/question_numerical.jinja +36 -36
- edsl/questions/{QuestionBaseGenMixin.py → question_base_gen_mixin.py} +168 -161
- edsl/questions/question_registry.py +177 -177
- edsl/questions/{RegisterQuestionsMeta.py → register_questions_meta.py} +71 -71
- edsl/questions/{ResponseValidatorABC.py → response_validator_abc.py} +188 -174
- edsl/questions/response_validator_factory.py +34 -0
- edsl/questions/settings.py +12 -12
- edsl/questions/templates/budget/answering_instructions.jinja +7 -7
- edsl/questions/templates/budget/question_presentation.jinja +7 -7
- edsl/questions/templates/checkbox/answering_instructions.jinja +10 -10
- edsl/questions/templates/checkbox/question_presentation.jinja +22 -22
- edsl/questions/templates/extract/answering_instructions.jinja +7 -7
- edsl/questions/templates/likert_five/answering_instructions.jinja +10 -10
- edsl/questions/templates/likert_five/question_presentation.jinja +11 -11
- edsl/questions/templates/linear_scale/answering_instructions.jinja +5 -5
- edsl/questions/templates/linear_scale/question_presentation.jinja +5 -5
- edsl/questions/templates/list/answering_instructions.jinja +3 -3
- edsl/questions/templates/list/question_presentation.jinja +5 -5
- 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/questions/templates/multiple_choice/answering_instructions.jinja +9 -9
- edsl/questions/templates/multiple_choice/question_presentation.jinja +11 -11
- edsl/questions/templates/numerical/answering_instructions.jinja +6 -6
- edsl/questions/templates/numerical/question_presentation.jinja +6 -6
- edsl/questions/templates/rank/answering_instructions.jinja +11 -11
- edsl/questions/templates/rank/question_presentation.jinja +15 -15
- edsl/questions/templates/top_k/answering_instructions.jinja +8 -8
- edsl/questions/templates/top_k/question_presentation.jinja +22 -22
- edsl/questions/templates/yes_no/answering_instructions.jinja +6 -6
- edsl/questions/templates/yes_no/question_presentation.jinja +11 -11
- edsl/results/CSSParameterizer.py +108 -108
- edsl/results/Dataset.py +587 -424
- edsl/results/DatasetExportMixin.py +594 -731
- edsl/results/DatasetTree.py +295 -275
- edsl/results/MarkdownToDocx.py +122 -0
- edsl/results/MarkdownToPDF.py +111 -0
- edsl/results/Result.py +557 -465
- edsl/results/Results.py +1183 -1165
- edsl/results/ResultsExportMixin.py +45 -43
- edsl/results/ResultsGGMixin.py +121 -121
- edsl/results/TableDisplay.py +125 -198
- edsl/results/TextEditor.py +50 -0
- edsl/results/__init__.py +2 -2
- edsl/results/file_exports.py +252 -0
- edsl/results/{ResultsFetchMixin.py → results_fetch_mixin.py} +33 -33
- edsl/results/{Selector.py → results_selector.py} +145 -135
- edsl/results/{ResultsToolsMixin.py → results_tools_mixin.py} +98 -98
- edsl/results/smart_objects.py +96 -0
- edsl/results/table_data_class.py +12 -0
- edsl/results/table_display.css +77 -77
- edsl/results/table_renderers.py +118 -0
- edsl/results/tree_explore.py +115 -115
- edsl/scenarios/ConstructDownloadLink.py +109 -0
- edsl/scenarios/DocumentChunker.py +102 -0
- edsl/scenarios/DocxScenario.py +16 -0
- edsl/scenarios/FileStore.py +511 -632
- edsl/scenarios/PdfExtractor.py +40 -0
- edsl/scenarios/Scenario.py +498 -601
- edsl/scenarios/ScenarioHtmlMixin.py +65 -64
- edsl/scenarios/ScenarioList.py +1458 -1287
- edsl/scenarios/ScenarioListExportMixin.py +45 -52
- edsl/scenarios/ScenarioListPdfMixin.py +239 -261
- edsl/scenarios/__init__.py +3 -4
- edsl/scenarios/directory_scanner.py +96 -0
- edsl/scenarios/file_methods.py +85 -0
- edsl/scenarios/handlers/__init__.py +13 -0
- edsl/scenarios/handlers/csv.py +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/scenarios/{ScenarioJoin.py → scenario_join.py} +131 -127
- edsl/scenarios/scenario_selector.py +156 -0
- edsl/shared.py +1 -1
- edsl/study/ObjectEntry.py +173 -173
- edsl/study/ProofOfWork.py +113 -113
- edsl/study/SnapShot.py +80 -80
- edsl/study/Study.py +521 -528
- edsl/study/__init__.py +4 -4
- edsl/surveys/ConstructDAG.py +92 -0
- edsl/surveys/DAG.py +148 -148
- edsl/surveys/EditSurvey.py +221 -0
- edsl/surveys/InstructionHandler.py +100 -0
- edsl/surveys/Memory.py +31 -31
- edsl/surveys/MemoryManagement.py +72 -0
- edsl/surveys/MemoryPlan.py +244 -244
- edsl/surveys/Rule.py +327 -326
- edsl/surveys/RuleCollection.py +385 -387
- edsl/surveys/RuleManager.py +172 -0
- edsl/surveys/Simulator.py +75 -0
- edsl/surveys/Survey.py +1280 -1801
- edsl/surveys/SurveyCSS.py +273 -261
- edsl/surveys/SurveyExportMixin.py +259 -259
- edsl/surveys/{SurveyFlowVisualizationMixin.py → SurveyFlowVisualization.py} +181 -179
- edsl/surveys/SurveyQualtricsImport.py +284 -284
- edsl/surveys/SurveyToApp.py +141 -0
- edsl/surveys/__init__.py +5 -3
- edsl/surveys/base.py +53 -53
- edsl/surveys/descriptors.py +60 -56
- edsl/surveys/instructions/ChangeInstruction.py +48 -49
- edsl/surveys/instructions/Instruction.py +56 -65
- edsl/surveys/instructions/InstructionCollection.py +82 -77
- edsl/templates/error_reporting/base.html +23 -23
- edsl/templates/error_reporting/exceptions_by_model.html +34 -34
- edsl/templates/error_reporting/exceptions_by_question_name.html +16 -16
- edsl/templates/error_reporting/exceptions_by_type.html +16 -16
- edsl/templates/error_reporting/interview_details.html +115 -115
- edsl/templates/error_reporting/interviews.html +19 -19
- edsl/templates/error_reporting/overview.html +4 -4
- edsl/templates/error_reporting/performance_plot.html +1 -1
- edsl/templates/error_reporting/report.css +73 -73
- edsl/templates/error_reporting/report.html +117 -117
- edsl/templates/error_reporting/report.js +25 -25
- edsl/tools/__init__.py +1 -1
- edsl/tools/clusters.py +192 -192
- edsl/tools/embeddings.py +27 -27
- edsl/tools/embeddings_plotting.py +118 -118
- edsl/tools/plotting.py +112 -112
- edsl/tools/summarize.py +18 -18
- edsl/utilities/PrettyList.py +56 -0
- edsl/utilities/SystemInfo.py +28 -28
- edsl/utilities/__init__.py +22 -22
- edsl/utilities/ast_utilities.py +25 -25
- edsl/utilities/data/Registry.py +6 -6
- edsl/utilities/data/__init__.py +1 -1
- edsl/utilities/data/scooter_results.json +1 -1
- edsl/utilities/decorators.py +77 -77
- edsl/utilities/gcp_bucket/cloud_storage.py +96 -96
- edsl/utilities/interface.py +627 -627
- edsl/utilities/is_notebook.py +18 -0
- edsl/utilities/is_valid_variable_name.py +11 -0
- edsl/utilities/naming_utilities.py +263 -263
- edsl/utilities/remove_edsl_version.py +24 -0
- edsl/utilities/repair_functions.py +28 -28
- edsl/utilities/restricted_python.py +70 -70
- edsl/utilities/utilities.py +436 -424
- {edsl-0.1.39.dev3.dist-info → edsl-0.1.39.dev5.dist-info}/LICENSE +21 -21
- {edsl-0.1.39.dev3.dist-info → edsl-0.1.39.dev5.dist-info}/METADATA +13 -11
- edsl-0.1.39.dev5.dist-info/RECORD +358 -0
- {edsl-0.1.39.dev3.dist-info → edsl-0.1.39.dev5.dist-info}/WHEEL +1 -1
- edsl/language_models/KeyLookup.py +0 -30
- edsl/language_models/registry.py +0 -190
- edsl/language_models/unused/ReplicateBase.py +0 -83
- edsl/results/ResultsDBMixin.py +0 -238
- edsl-0.1.39.dev3.dist-info/RECORD +0 -277
@@ -1,359 +1,359 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
import random
|
3
|
-
from typing import Any, Optional, Union
|
4
|
-
|
5
|
-
from jinja2 import Template
|
6
|
-
|
7
|
-
from edsl.questions.QuestionBase import QuestionBase
|
8
|
-
from edsl.questions.descriptors import (
|
9
|
-
IntegerDescriptor,
|
10
|
-
QuestionOptionsDescriptor,
|
11
|
-
)
|
12
|
-
|
13
|
-
from edsl.questions.decorators import inject_exception
|
14
|
-
|
15
|
-
from pydantic import field_validator
|
16
|
-
from edsl.questions.
|
17
|
-
from edsl.questions.
|
18
|
-
|
19
|
-
from edsl.exceptions import QuestionAnswerValidationError
|
20
|
-
|
21
|
-
from pydantic import BaseModel, Field, conlist
|
22
|
-
from typing import List, Literal, Optional, Annotated
|
23
|
-
|
24
|
-
|
25
|
-
def create_checkbox_response_model(
|
26
|
-
choices: list,
|
27
|
-
min_selections: Optional[int] = None,
|
28
|
-
max_selections: Optional[int] = None,
|
29
|
-
permissive: bool = False,
|
30
|
-
):
|
31
|
-
"""
|
32
|
-
Dynamically create a CheckboxResponse model with a predefined list of choices.
|
33
|
-
|
34
|
-
:param choices: A list of allowed values for the answer field.
|
35
|
-
:param include_comment: Whether to include a comment field in the model.
|
36
|
-
:return: A new Pydantic model class.
|
37
|
-
"""
|
38
|
-
# Convert the choices list to a tuple for use with Literal
|
39
|
-
choice_tuple = tuple(choices)
|
40
|
-
|
41
|
-
field_params = {}
|
42
|
-
if min_selections is not None and not permissive:
|
43
|
-
field_params["min_items"] = min_selections
|
44
|
-
if max_selections is not None and not permissive:
|
45
|
-
field_params["max_items"] = max_selections
|
46
|
-
|
47
|
-
class CheckboxResponse(BaseModel):
|
48
|
-
answer: Annotated[
|
49
|
-
List[Literal[choice_tuple]],
|
50
|
-
Field(..., **field_params),
|
51
|
-
] = Field(..., description="List of selected choices")
|
52
|
-
comment: Optional[str] = Field(None, description="Optional comment field")
|
53
|
-
generated_tokens: Optional[Any] = Field(default=None)
|
54
|
-
|
55
|
-
class Config:
|
56
|
-
@staticmethod
|
57
|
-
def json_schema_extra(schema: dict, model: BaseModel) -> None:
|
58
|
-
# Add the list of choices to the schema for better documentation
|
59
|
-
for prop in schema.get("properties", {}).values():
|
60
|
-
if prop.get("title") == "answer":
|
61
|
-
prop["items"] = {"enum": choices}
|
62
|
-
|
63
|
-
return CheckboxResponse
|
64
|
-
|
65
|
-
|
66
|
-
class CheckBoxResponseValidator(ResponseValidatorABC):
|
67
|
-
required_params = [
|
68
|
-
"question_options",
|
69
|
-
"min_selections",
|
70
|
-
"max_selections",
|
71
|
-
"use_code",
|
72
|
-
"permissive",
|
73
|
-
]
|
74
|
-
|
75
|
-
valid_examples = [
|
76
|
-
({"answer": [1, 2]}, {"question_options": ["Good", "Great", "OK", "Bad"]})
|
77
|
-
]
|
78
|
-
|
79
|
-
invalid_examples = [
|
80
|
-
(
|
81
|
-
{"answer": [-1]},
|
82
|
-
{"question_options": ["Good", "Great", "OK", "Bad"]},
|
83
|
-
"Answer code must be a non-negative integer",
|
84
|
-
),
|
85
|
-
(
|
86
|
-
{"answer": 1},
|
87
|
-
{"question_options": ["Good", "Great", "OK", "Bad"]},
|
88
|
-
"Answer code must be a list",
|
89
|
-
),
|
90
|
-
(
|
91
|
-
{"answer": [1, 2, 3, 4]},
|
92
|
-
{
|
93
|
-
"question_options": ["Good", "Great", "OK", "Bad"],
|
94
|
-
"min_selections": 1,
|
95
|
-
"max_selections": 2,
|
96
|
-
},
|
97
|
-
"Too many options selected",
|
98
|
-
),
|
99
|
-
]
|
100
|
-
|
101
|
-
def fix(self, response, verbose=False):
|
102
|
-
if verbose:
|
103
|
-
print("Invalid response of QuestionCheckBox was: ", response)
|
104
|
-
response_text = response.get("generated_tokens")
|
105
|
-
if response_text is None or response_text == "": # nothing to be done
|
106
|
-
return response
|
107
|
-
# Maybe it's a comma separated list?
|
108
|
-
proposed_list = response_text.split(",")
|
109
|
-
proposed_list = [item.strip() for item in proposed_list]
|
110
|
-
if verbose:
|
111
|
-
print("Using code? ", self.use_code)
|
112
|
-
if self.use_code:
|
113
|
-
try:
|
114
|
-
proposed_list = [int(i) for i in proposed_list]
|
115
|
-
except ValueError:
|
116
|
-
# print("Could not convert to int")
|
117
|
-
pass
|
118
|
-
|
119
|
-
if verbose:
|
120
|
-
print("Proposed solution is: ", proposed_list)
|
121
|
-
|
122
|
-
# print(f"Ivalid generated tokens was was: {response_text}")
|
123
|
-
if "comment" in response:
|
124
|
-
proposed_data = {
|
125
|
-
"answer": proposed_list,
|
126
|
-
"comment": response["comment"],
|
127
|
-
"generated_tokens": response.get("generated_tokens", None),
|
128
|
-
}
|
129
|
-
else:
|
130
|
-
proposed_data = {
|
131
|
-
"answer": proposed_list,
|
132
|
-
"generated_tokens": response.get("generated_tokens", None),
|
133
|
-
}
|
134
|
-
|
135
|
-
try:
|
136
|
-
self.response_model(**proposed_data)
|
137
|
-
print("Proposed solution is valid")
|
138
|
-
print("Returning proposed data: ", proposed_data)
|
139
|
-
return proposed_data
|
140
|
-
except Exception as e:
|
141
|
-
if verbose:
|
142
|
-
print(f"Proposed solution {proposed_data} is invalid. Error: {e}")
|
143
|
-
# return response
|
144
|
-
if verbose:
|
145
|
-
print("Now seeing if responses show up in the answer")
|
146
|
-
matches = []
|
147
|
-
for index, option in enumerate(self.question_options):
|
148
|
-
if self.use_code:
|
149
|
-
if str(index) in response_text:
|
150
|
-
matches.append(index)
|
151
|
-
else:
|
152
|
-
if option in response_text:
|
153
|
-
matches.append(index)
|
154
|
-
proposed_data = {
|
155
|
-
"answer": matches,
|
156
|
-
"comment": response.get("comment", None),
|
157
|
-
"generated_tokens": response.get("generated_tokens", None),
|
158
|
-
}
|
159
|
-
try:
|
160
|
-
self.response_model(**proposed_data)
|
161
|
-
return proposed_data
|
162
|
-
except Exception as e:
|
163
|
-
if verbose:
|
164
|
-
print(f"Proposed solution {proposed_data} is invalid. Error: {e}")
|
165
|
-
return response
|
166
|
-
|
167
|
-
def custom_validate(self, response) -> BaseResponse:
|
168
|
-
if response.answer is None:
|
169
|
-
raise QuestionAnswerValidationError("Answer is missing.")
|
170
|
-
return response.dict()
|
171
|
-
|
172
|
-
|
173
|
-
class QuestionCheckBox(QuestionBase):
|
174
|
-
"""This question prompts the agent to select options from a list."""
|
175
|
-
|
176
|
-
question_type = "checkbox"
|
177
|
-
purpose = "When options are known and limited"
|
178
|
-
question_options: list[str] = QuestionOptionsDescriptor()
|
179
|
-
min_selections = IntegerDescriptor(none_allowed=True)
|
180
|
-
max_selections = IntegerDescriptor(none_allowed=True)
|
181
|
-
|
182
|
-
_response_model = None
|
183
|
-
response_validator_class = CheckBoxResponseValidator
|
184
|
-
|
185
|
-
def __init__(
|
186
|
-
self,
|
187
|
-
question_name: str,
|
188
|
-
question_text: str,
|
189
|
-
question_options: list[str],
|
190
|
-
min_selections: Optional[int] = None,
|
191
|
-
max_selections: Optional[int] = None,
|
192
|
-
include_comment: bool = True,
|
193
|
-
use_code: bool = True,
|
194
|
-
question_presentation: Optional[str] = None,
|
195
|
-
answering_instructions: Optional[str] = None,
|
196
|
-
permissive: bool = False,
|
197
|
-
):
|
198
|
-
"""Instantiate a new QuestionCheckBox.
|
199
|
-
|
200
|
-
:param question_name: The name of the question.
|
201
|
-
:param question_text: The text of the question.
|
202
|
-
:param question_options: The options the respondent should select from.
|
203
|
-
:param min_selections: The minimum number of options that must be selected.
|
204
|
-
:param max_selections: The maximum number of options that must be selected.
|
205
|
-
"""
|
206
|
-
self.question_name = question_name
|
207
|
-
self.question_text = question_text
|
208
|
-
self.min_selections = min_selections
|
209
|
-
self.max_selections = max_selections
|
210
|
-
self.question_options = question_options
|
211
|
-
|
212
|
-
self._include_comment = include_comment
|
213
|
-
self._use_code = use_code
|
214
|
-
self.permissive = permissive
|
215
|
-
|
216
|
-
self.question_presentation = question_presentation
|
217
|
-
self.answering_instructions = answering_instructions
|
218
|
-
|
219
|
-
def create_response_model(self):
|
220
|
-
if not self._use_code:
|
221
|
-
return create_checkbox_response_model(
|
222
|
-
self.question_options,
|
223
|
-
min_selections=self.min_selections,
|
224
|
-
max_selections=self.max_selections, # include_comment=self._include_comment
|
225
|
-
permissive=self.permissive,
|
226
|
-
)
|
227
|
-
else:
|
228
|
-
return create_checkbox_response_model(
|
229
|
-
list(range(len(self.question_options))),
|
230
|
-
min_selections=self.min_selections,
|
231
|
-
max_selections=self.max_selections, # include_comment=self._include_comment
|
232
|
-
permissive=self.permissive,
|
233
|
-
)
|
234
|
-
|
235
|
-
def _translate_answer_code_to_answer(
|
236
|
-
self, answer_codes, scenario: "Scenario" = None
|
237
|
-
):
|
238
|
-
"""
|
239
|
-
Translate the answer code to the actual answer.
|
240
|
-
|
241
|
-
For example, for question options ["a", "b", "c"],the answer codes are 0, 1, and 2.
|
242
|
-
The LLM will respond with [0,1] and this code will translate it to ["a","b"].
|
243
|
-
"""
|
244
|
-
from edsl.scenarios.Scenario import Scenario
|
245
|
-
|
246
|
-
scenario = scenario or Scenario()
|
247
|
-
translated_options = [
|
248
|
-
Template(str(option)).render(scenario) for option in self.question_options
|
249
|
-
]
|
250
|
-
translated_codes = []
|
251
|
-
for answer_code in answer_codes:
|
252
|
-
if self._use_code:
|
253
|
-
translated_codes.append(translated_options[int(answer_code)])
|
254
|
-
else:
|
255
|
-
translated_codes.append(answer_code)
|
256
|
-
return translated_codes
|
257
|
-
|
258
|
-
# def _simulate_answer(self, human_readable=True) -> dict[str, Union[int, str]]:
|
259
|
-
# """Simulate a valid answer for debugging purposes."""
|
260
|
-
# from edsl.utilities.utilities import random_string
|
261
|
-
|
262
|
-
# min_selections = self.min_selections or 1
|
263
|
-
# max_selections = self.max_selections or len(self.question_options)
|
264
|
-
# num_selections = random.randint(min_selections, max_selections)
|
265
|
-
# if human_readable:
|
266
|
-
# # Select a random number of options from self.question_options
|
267
|
-
# selected_options = random.sample(self.question_options, num_selections)
|
268
|
-
# answer = {
|
269
|
-
# "answer": selected_options,
|
270
|
-
# "comment": random_string(),
|
271
|
-
# }
|
272
|
-
# else:
|
273
|
-
# # Select a random number of indices from the range of self.question_options
|
274
|
-
# selected_indices = random.sample(
|
275
|
-
# range(len(self.question_options)), num_selections
|
276
|
-
# )
|
277
|
-
# answer = {
|
278
|
-
# "answer": selected_indices,
|
279
|
-
# "comment": random_string(),
|
280
|
-
# }
|
281
|
-
# return answer
|
282
|
-
|
283
|
-
@property
|
284
|
-
def question_html_content(self) -> str:
|
285
|
-
instructions = ""
|
286
|
-
if self.min_selections is not None:
|
287
|
-
instructions += f"Select at least {self.min_selections} option(s). "
|
288
|
-
if self.max_selections is not None:
|
289
|
-
instructions += f"Select at most {self.max_selections} option(s)."
|
290
|
-
question_html_content = Template(
|
291
|
-
"""
|
292
|
-
<p>{{ instructions }}</p>
|
293
|
-
{% for option in question_options %}
|
294
|
-
<div>
|
295
|
-
<input type="checkbox" id="{{ option }}" name="{{ question_name }}" value="{{ option }}">
|
296
|
-
<label for="{{ option }}">{{ option }}</label>
|
297
|
-
</div>
|
298
|
-
{% endfor %}
|
299
|
-
"""
|
300
|
-
).render(
|
301
|
-
instructions=instructions,
|
302
|
-
question_name=self.question_name,
|
303
|
-
question_options=self.question_options,
|
304
|
-
)
|
305
|
-
return question_html_content
|
306
|
-
|
307
|
-
################
|
308
|
-
# Helpful methods
|
309
|
-
################
|
310
|
-
@classmethod
|
311
|
-
@inject_exception
|
312
|
-
def example(cls, include_comment=False, use_code=True) -> QuestionCheckBox:
|
313
|
-
"""Return an example checkbox question."""
|
314
|
-
return cls(
|
315
|
-
question_name="never_eat",
|
316
|
-
question_text="Which of the following foods would you eat if you had to?",
|
317
|
-
question_options=[
|
318
|
-
"soggy meatpie",
|
319
|
-
"rare snails",
|
320
|
-
"mouldy bread",
|
321
|
-
"panda milk custard",
|
322
|
-
"McDonalds",
|
323
|
-
],
|
324
|
-
min_selections=2,
|
325
|
-
max_selections=5,
|
326
|
-
use_code=use_code,
|
327
|
-
include_comment=include_comment,
|
328
|
-
)
|
329
|
-
|
330
|
-
|
331
|
-
def main():
|
332
|
-
"""Create an example QuestionCheckBox and test its methods."""
|
333
|
-
from edsl.questions.QuestionCheckBox import QuestionCheckBox
|
334
|
-
|
335
|
-
q = QuestionCheckBox.example()
|
336
|
-
q.question_text
|
337
|
-
q.question_options
|
338
|
-
q.question_name
|
339
|
-
# validate an answer
|
340
|
-
q._validate_answer({"answer": [1, 2], "comment": "I like custard"})
|
341
|
-
# translate answer code
|
342
|
-
q._translate_answer_code_to_answer([1, 2])
|
343
|
-
# simulate answer
|
344
|
-
q._simulate_answer()
|
345
|
-
q._simulate_answer(human_readable=False)
|
346
|
-
q._validate_answer(q._simulate_answer(human_readable=False))
|
347
|
-
# serialization (inherits from Question)
|
348
|
-
q.to_dict()
|
349
|
-
assert q.from_dict(q.to_dict()) == q
|
350
|
-
|
351
|
-
import doctest
|
352
|
-
|
353
|
-
doctest.testmod(optionflags=doctest.ELLIPSIS)
|
354
|
-
|
355
|
-
|
356
|
-
if __name__ == "__main__":
|
357
|
-
import doctest
|
358
|
-
|
359
|
-
doctest.testmod(optionflags=doctest.ELLIPSIS)
|
1
|
+
from __future__ import annotations
|
2
|
+
import random
|
3
|
+
from typing import Any, Optional, Union
|
4
|
+
|
5
|
+
from jinja2 import Template
|
6
|
+
|
7
|
+
from edsl.questions.QuestionBase import QuestionBase
|
8
|
+
from edsl.questions.descriptors import (
|
9
|
+
IntegerDescriptor,
|
10
|
+
QuestionOptionsDescriptor,
|
11
|
+
)
|
12
|
+
|
13
|
+
from edsl.questions.decorators import inject_exception
|
14
|
+
|
15
|
+
from pydantic import field_validator
|
16
|
+
from edsl.questions.response_validator_abc import ResponseValidatorABC
|
17
|
+
from edsl.questions.data_structures import BaseResponse
|
18
|
+
|
19
|
+
from edsl.exceptions.questions import QuestionAnswerValidationError
|
20
|
+
|
21
|
+
from pydantic import BaseModel, Field, conlist
|
22
|
+
from typing import List, Literal, Optional, Annotated
|
23
|
+
|
24
|
+
|
25
|
+
def create_checkbox_response_model(
|
26
|
+
choices: list,
|
27
|
+
min_selections: Optional[int] = None,
|
28
|
+
max_selections: Optional[int] = None,
|
29
|
+
permissive: bool = False,
|
30
|
+
):
|
31
|
+
"""
|
32
|
+
Dynamically create a CheckboxResponse model with a predefined list of choices.
|
33
|
+
|
34
|
+
:param choices: A list of allowed values for the answer field.
|
35
|
+
:param include_comment: Whether to include a comment field in the model.
|
36
|
+
:return: A new Pydantic model class.
|
37
|
+
"""
|
38
|
+
# Convert the choices list to a tuple for use with Literal
|
39
|
+
choice_tuple = tuple(choices)
|
40
|
+
|
41
|
+
field_params = {}
|
42
|
+
if min_selections is not None and not permissive:
|
43
|
+
field_params["min_items"] = min_selections
|
44
|
+
if max_selections is not None and not permissive:
|
45
|
+
field_params["max_items"] = max_selections
|
46
|
+
|
47
|
+
class CheckboxResponse(BaseModel):
|
48
|
+
answer: Annotated[
|
49
|
+
List[Literal[choice_tuple]],
|
50
|
+
Field(..., **field_params),
|
51
|
+
] = Field(..., description="List of selected choices")
|
52
|
+
comment: Optional[str] = Field(None, description="Optional comment field")
|
53
|
+
generated_tokens: Optional[Any] = Field(default=None)
|
54
|
+
|
55
|
+
class Config:
|
56
|
+
@staticmethod
|
57
|
+
def json_schema_extra(schema: dict, model: BaseModel) -> None:
|
58
|
+
# Add the list of choices to the schema for better documentation
|
59
|
+
for prop in schema.get("properties", {}).values():
|
60
|
+
if prop.get("title") == "answer":
|
61
|
+
prop["items"] = {"enum": choices}
|
62
|
+
|
63
|
+
return CheckboxResponse
|
64
|
+
|
65
|
+
|
66
|
+
class CheckBoxResponseValidator(ResponseValidatorABC):
|
67
|
+
required_params = [
|
68
|
+
"question_options",
|
69
|
+
"min_selections",
|
70
|
+
"max_selections",
|
71
|
+
"use_code",
|
72
|
+
"permissive",
|
73
|
+
]
|
74
|
+
|
75
|
+
valid_examples = [
|
76
|
+
({"answer": [1, 2]}, {"question_options": ["Good", "Great", "OK", "Bad"]})
|
77
|
+
]
|
78
|
+
|
79
|
+
invalid_examples = [
|
80
|
+
(
|
81
|
+
{"answer": [-1]},
|
82
|
+
{"question_options": ["Good", "Great", "OK", "Bad"]},
|
83
|
+
"Answer code must be a non-negative integer",
|
84
|
+
),
|
85
|
+
(
|
86
|
+
{"answer": 1},
|
87
|
+
{"question_options": ["Good", "Great", "OK", "Bad"]},
|
88
|
+
"Answer code must be a list",
|
89
|
+
),
|
90
|
+
(
|
91
|
+
{"answer": [1, 2, 3, 4]},
|
92
|
+
{
|
93
|
+
"question_options": ["Good", "Great", "OK", "Bad"],
|
94
|
+
"min_selections": 1,
|
95
|
+
"max_selections": 2,
|
96
|
+
},
|
97
|
+
"Too many options selected",
|
98
|
+
),
|
99
|
+
]
|
100
|
+
|
101
|
+
def fix(self, response, verbose=False):
|
102
|
+
if verbose:
|
103
|
+
print("Invalid response of QuestionCheckBox was: ", response)
|
104
|
+
response_text = response.get("generated_tokens")
|
105
|
+
if response_text is None or response_text == "": # nothing to be done
|
106
|
+
return response
|
107
|
+
# Maybe it's a comma separated list?
|
108
|
+
proposed_list = response_text.split(",")
|
109
|
+
proposed_list = [item.strip() for item in proposed_list]
|
110
|
+
if verbose:
|
111
|
+
print("Using code? ", self.use_code)
|
112
|
+
if self.use_code:
|
113
|
+
try:
|
114
|
+
proposed_list = [int(i) for i in proposed_list]
|
115
|
+
except ValueError:
|
116
|
+
# print("Could not convert to int")
|
117
|
+
pass
|
118
|
+
|
119
|
+
if verbose:
|
120
|
+
print("Proposed solution is: ", proposed_list)
|
121
|
+
|
122
|
+
# print(f"Ivalid generated tokens was was: {response_text}")
|
123
|
+
if "comment" in response:
|
124
|
+
proposed_data = {
|
125
|
+
"answer": proposed_list,
|
126
|
+
"comment": response["comment"],
|
127
|
+
"generated_tokens": response.get("generated_tokens", None),
|
128
|
+
}
|
129
|
+
else:
|
130
|
+
proposed_data = {
|
131
|
+
"answer": proposed_list,
|
132
|
+
"generated_tokens": response.get("generated_tokens", None),
|
133
|
+
}
|
134
|
+
|
135
|
+
try:
|
136
|
+
self.response_model(**proposed_data)
|
137
|
+
print("Proposed solution is valid")
|
138
|
+
print("Returning proposed data: ", proposed_data)
|
139
|
+
return proposed_data
|
140
|
+
except Exception as e:
|
141
|
+
if verbose:
|
142
|
+
print(f"Proposed solution {proposed_data} is invalid. Error: {e}")
|
143
|
+
# return response
|
144
|
+
if verbose:
|
145
|
+
print("Now seeing if responses show up in the answer")
|
146
|
+
matches = []
|
147
|
+
for index, option in enumerate(self.question_options):
|
148
|
+
if self.use_code:
|
149
|
+
if str(index) in response_text:
|
150
|
+
matches.append(index)
|
151
|
+
else:
|
152
|
+
if option in response_text:
|
153
|
+
matches.append(index)
|
154
|
+
proposed_data = {
|
155
|
+
"answer": matches,
|
156
|
+
"comment": response.get("comment", None),
|
157
|
+
"generated_tokens": response.get("generated_tokens", None),
|
158
|
+
}
|
159
|
+
try:
|
160
|
+
self.response_model(**proposed_data)
|
161
|
+
return proposed_data
|
162
|
+
except Exception as e:
|
163
|
+
if verbose:
|
164
|
+
print(f"Proposed solution {proposed_data} is invalid. Error: {e}")
|
165
|
+
return response
|
166
|
+
|
167
|
+
def custom_validate(self, response) -> BaseResponse:
|
168
|
+
if response.answer is None:
|
169
|
+
raise QuestionAnswerValidationError("Answer is missing.")
|
170
|
+
return response.dict()
|
171
|
+
|
172
|
+
|
173
|
+
class QuestionCheckBox(QuestionBase):
|
174
|
+
"""This question prompts the agent to select options from a list."""
|
175
|
+
|
176
|
+
question_type = "checkbox"
|
177
|
+
purpose = "When options are known and limited"
|
178
|
+
question_options: list[str] = QuestionOptionsDescriptor()
|
179
|
+
min_selections = IntegerDescriptor(none_allowed=True)
|
180
|
+
max_selections = IntegerDescriptor(none_allowed=True)
|
181
|
+
|
182
|
+
_response_model = None
|
183
|
+
response_validator_class = CheckBoxResponseValidator
|
184
|
+
|
185
|
+
def __init__(
|
186
|
+
self,
|
187
|
+
question_name: str,
|
188
|
+
question_text: str,
|
189
|
+
question_options: list[str],
|
190
|
+
min_selections: Optional[int] = None,
|
191
|
+
max_selections: Optional[int] = None,
|
192
|
+
include_comment: bool = True,
|
193
|
+
use_code: bool = True,
|
194
|
+
question_presentation: Optional[str] = None,
|
195
|
+
answering_instructions: Optional[str] = None,
|
196
|
+
permissive: bool = False,
|
197
|
+
):
|
198
|
+
"""Instantiate a new QuestionCheckBox.
|
199
|
+
|
200
|
+
:param question_name: The name of the question.
|
201
|
+
:param question_text: The text of the question.
|
202
|
+
:param question_options: The options the respondent should select from.
|
203
|
+
:param min_selections: The minimum number of options that must be selected.
|
204
|
+
:param max_selections: The maximum number of options that must be selected.
|
205
|
+
"""
|
206
|
+
self.question_name = question_name
|
207
|
+
self.question_text = question_text
|
208
|
+
self.min_selections = min_selections
|
209
|
+
self.max_selections = max_selections
|
210
|
+
self.question_options = question_options
|
211
|
+
|
212
|
+
self._include_comment = include_comment
|
213
|
+
self._use_code = use_code
|
214
|
+
self.permissive = permissive
|
215
|
+
|
216
|
+
self.question_presentation = question_presentation
|
217
|
+
self.answering_instructions = answering_instructions
|
218
|
+
|
219
|
+
def create_response_model(self):
|
220
|
+
if not self._use_code:
|
221
|
+
return create_checkbox_response_model(
|
222
|
+
self.question_options,
|
223
|
+
min_selections=self.min_selections,
|
224
|
+
max_selections=self.max_selections, # include_comment=self._include_comment
|
225
|
+
permissive=self.permissive,
|
226
|
+
)
|
227
|
+
else:
|
228
|
+
return create_checkbox_response_model(
|
229
|
+
list(range(len(self.question_options))),
|
230
|
+
min_selections=self.min_selections,
|
231
|
+
max_selections=self.max_selections, # include_comment=self._include_comment
|
232
|
+
permissive=self.permissive,
|
233
|
+
)
|
234
|
+
|
235
|
+
def _translate_answer_code_to_answer(
|
236
|
+
self, answer_codes, scenario: "Scenario" = None
|
237
|
+
):
|
238
|
+
"""
|
239
|
+
Translate the answer code to the actual answer.
|
240
|
+
|
241
|
+
For example, for question options ["a", "b", "c"],the answer codes are 0, 1, and 2.
|
242
|
+
The LLM will respond with [0,1] and this code will translate it to ["a","b"].
|
243
|
+
"""
|
244
|
+
from edsl.scenarios.Scenario import Scenario
|
245
|
+
|
246
|
+
scenario = scenario or Scenario()
|
247
|
+
translated_options = [
|
248
|
+
Template(str(option)).render(scenario) for option in self.question_options
|
249
|
+
]
|
250
|
+
translated_codes = []
|
251
|
+
for answer_code in answer_codes:
|
252
|
+
if self._use_code:
|
253
|
+
translated_codes.append(translated_options[int(answer_code)])
|
254
|
+
else:
|
255
|
+
translated_codes.append(answer_code)
|
256
|
+
return translated_codes
|
257
|
+
|
258
|
+
# def _simulate_answer(self, human_readable=True) -> dict[str, Union[int, str]]:
|
259
|
+
# """Simulate a valid answer for debugging purposes."""
|
260
|
+
# from edsl.utilities.utilities import random_string
|
261
|
+
|
262
|
+
# min_selections = self.min_selections or 1
|
263
|
+
# max_selections = self.max_selections or len(self.question_options)
|
264
|
+
# num_selections = random.randint(min_selections, max_selections)
|
265
|
+
# if human_readable:
|
266
|
+
# # Select a random number of options from self.question_options
|
267
|
+
# selected_options = random.sample(self.question_options, num_selections)
|
268
|
+
# answer = {
|
269
|
+
# "answer": selected_options,
|
270
|
+
# "comment": random_string(),
|
271
|
+
# }
|
272
|
+
# else:
|
273
|
+
# # Select a random number of indices from the range of self.question_options
|
274
|
+
# selected_indices = random.sample(
|
275
|
+
# range(len(self.question_options)), num_selections
|
276
|
+
# )
|
277
|
+
# answer = {
|
278
|
+
# "answer": selected_indices,
|
279
|
+
# "comment": random_string(),
|
280
|
+
# }
|
281
|
+
# return answer
|
282
|
+
|
283
|
+
@property
|
284
|
+
def question_html_content(self) -> str:
|
285
|
+
instructions = ""
|
286
|
+
if self.min_selections is not None:
|
287
|
+
instructions += f"Select at least {self.min_selections} option(s). "
|
288
|
+
if self.max_selections is not None:
|
289
|
+
instructions += f"Select at most {self.max_selections} option(s)."
|
290
|
+
question_html_content = Template(
|
291
|
+
"""
|
292
|
+
<p>{{ instructions }}</p>
|
293
|
+
{% for option in question_options %}
|
294
|
+
<div>
|
295
|
+
<input type="checkbox" id="{{ option }}" name="{{ question_name }}" value="{{ option }}">
|
296
|
+
<label for="{{ option }}">{{ option }}</label>
|
297
|
+
</div>
|
298
|
+
{% endfor %}
|
299
|
+
"""
|
300
|
+
).render(
|
301
|
+
instructions=instructions,
|
302
|
+
question_name=self.question_name,
|
303
|
+
question_options=self.question_options,
|
304
|
+
)
|
305
|
+
return question_html_content
|
306
|
+
|
307
|
+
################
|
308
|
+
# Helpful methods
|
309
|
+
################
|
310
|
+
@classmethod
|
311
|
+
@inject_exception
|
312
|
+
def example(cls, include_comment=False, use_code=True) -> QuestionCheckBox:
|
313
|
+
"""Return an example checkbox question."""
|
314
|
+
return cls(
|
315
|
+
question_name="never_eat",
|
316
|
+
question_text="Which of the following foods would you eat if you had to?",
|
317
|
+
question_options=[
|
318
|
+
"soggy meatpie",
|
319
|
+
"rare snails",
|
320
|
+
"mouldy bread",
|
321
|
+
"panda milk custard",
|
322
|
+
"McDonalds",
|
323
|
+
],
|
324
|
+
min_selections=2,
|
325
|
+
max_selections=5,
|
326
|
+
use_code=use_code,
|
327
|
+
include_comment=include_comment,
|
328
|
+
)
|
329
|
+
|
330
|
+
|
331
|
+
def main():
|
332
|
+
"""Create an example QuestionCheckBox and test its methods."""
|
333
|
+
from edsl.questions.QuestionCheckBox import QuestionCheckBox
|
334
|
+
|
335
|
+
q = QuestionCheckBox.example()
|
336
|
+
q.question_text
|
337
|
+
q.question_options
|
338
|
+
q.question_name
|
339
|
+
# validate an answer
|
340
|
+
q._validate_answer({"answer": [1, 2], "comment": "I like custard"})
|
341
|
+
# translate answer code
|
342
|
+
q._translate_answer_code_to_answer([1, 2])
|
343
|
+
# simulate answer
|
344
|
+
q._simulate_answer()
|
345
|
+
q._simulate_answer(human_readable=False)
|
346
|
+
q._validate_answer(q._simulate_answer(human_readable=False))
|
347
|
+
# serialization (inherits from Question)
|
348
|
+
q.to_dict()
|
349
|
+
assert q.from_dict(q.to_dict()) == q
|
350
|
+
|
351
|
+
import doctest
|
352
|
+
|
353
|
+
doctest.testmod(optionflags=doctest.ELLIPSIS)
|
354
|
+
|
355
|
+
|
356
|
+
if __name__ == "__main__":
|
357
|
+
import doctest
|
358
|
+
|
359
|
+
doctest.testmod(optionflags=doctest.ELLIPSIS)
|