edsl 0.1.15__py3-none-any.whl → 0.1.40__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 +348 -38
- edsl/BaseDiff.py +260 -0
- edsl/TemplateLoader.py +24 -0
- edsl/__init__.py +45 -10
- edsl/__version__.py +1 -1
- edsl/agents/Agent.py +842 -144
- edsl/agents/AgentList.py +521 -25
- edsl/agents/Invigilator.py +250 -374
- edsl/agents/InvigilatorBase.py +257 -0
- edsl/agents/PromptConstructor.py +272 -0
- edsl/agents/QuestionInstructionPromptBuilder.py +128 -0
- edsl/agents/QuestionTemplateReplacementsBuilder.py +137 -0
- edsl/agents/descriptors.py +43 -13
- edsl/agents/prompt_helpers.py +129 -0
- edsl/agents/question_option_processor.py +172 -0
- edsl/auto/AutoStudy.py +130 -0
- edsl/auto/StageBase.py +243 -0
- edsl/auto/StageGenerateSurvey.py +178 -0
- edsl/auto/StageLabelQuestions.py +125 -0
- edsl/auto/StagePersona.py +61 -0
- edsl/auto/StagePersonaDimensionValueRanges.py +88 -0
- edsl/auto/StagePersonaDimensionValues.py +74 -0
- edsl/auto/StagePersonaDimensions.py +69 -0
- edsl/auto/StageQuestions.py +74 -0
- edsl/auto/SurveyCreatorPipeline.py +21 -0
- edsl/auto/utilities.py +218 -0
- edsl/base/Base.py +279 -0
- edsl/config.py +115 -113
- edsl/conversation/Conversation.py +290 -0
- edsl/conversation/car_buying.py +59 -0
- edsl/conversation/chips.py +95 -0
- edsl/conversation/mug_negotiation.py +81 -0
- edsl/conversation/next_speaker_utilities.py +93 -0
- edsl/coop/CoopFunctionsMixin.py +15 -0
- edsl/coop/ExpectedParrotKeyHandler.py +125 -0
- edsl/coop/PriceFetcher.py +54 -0
- edsl/coop/__init__.py +1 -0
- edsl/coop/coop.py +1029 -134
- edsl/coop/utils.py +131 -0
- edsl/data/Cache.py +560 -89
- edsl/data/CacheEntry.py +230 -0
- edsl/data/CacheHandler.py +168 -0
- edsl/data/RemoteCacheSync.py +186 -0
- edsl/data/SQLiteDict.py +292 -0
- edsl/data/__init__.py +5 -3
- edsl/data/orm.py +6 -33
- edsl/data_transfer_models.py +74 -27
- edsl/enums.py +165 -8
- edsl/exceptions/BaseException.py +21 -0
- edsl/exceptions/__init__.py +52 -46
- edsl/exceptions/agents.py +33 -15
- edsl/exceptions/cache.py +5 -0
- edsl/exceptions/coop.py +8 -0
- edsl/exceptions/general.py +34 -0
- edsl/exceptions/inference_services.py +5 -0
- edsl/exceptions/jobs.py +15 -0
- edsl/exceptions/language_models.py +46 -1
- edsl/exceptions/questions.py +80 -5
- edsl/exceptions/results.py +16 -5
- edsl/exceptions/scenarios.py +29 -0
- edsl/exceptions/surveys.py +13 -10
- edsl/inference_services/AnthropicService.py +106 -0
- edsl/inference_services/AvailableModelCacheHandler.py +184 -0
- edsl/inference_services/AvailableModelFetcher.py +215 -0
- edsl/inference_services/AwsBedrock.py +118 -0
- edsl/inference_services/AzureAI.py +215 -0
- edsl/inference_services/DeepInfraService.py +18 -0
- edsl/inference_services/GoogleService.py +143 -0
- edsl/inference_services/GroqService.py +20 -0
- edsl/inference_services/InferenceServiceABC.py +80 -0
- edsl/inference_services/InferenceServicesCollection.py +138 -0
- edsl/inference_services/MistralAIService.py +120 -0
- edsl/inference_services/OllamaService.py +18 -0
- edsl/inference_services/OpenAIService.py +236 -0
- edsl/inference_services/PerplexityService.py +160 -0
- edsl/inference_services/ServiceAvailability.py +135 -0
- edsl/inference_services/TestService.py +90 -0
- edsl/inference_services/TogetherAIService.py +172 -0
- edsl/inference_services/data_structures.py +134 -0
- edsl/inference_services/models_available_cache.py +118 -0
- edsl/inference_services/rate_limits_cache.py +25 -0
- edsl/inference_services/registry.py +41 -0
- edsl/inference_services/write_available.py +10 -0
- edsl/jobs/AnswerQuestionFunctionConstructor.py +223 -0
- edsl/jobs/Answers.py +21 -20
- edsl/jobs/FetchInvigilator.py +47 -0
- edsl/jobs/InterviewTaskManager.py +98 -0
- edsl/jobs/InterviewsConstructor.py +50 -0
- edsl/jobs/Jobs.py +684 -206
- edsl/jobs/JobsChecks.py +172 -0
- edsl/jobs/JobsComponentConstructor.py +189 -0
- edsl/jobs/JobsPrompts.py +270 -0
- edsl/jobs/JobsRemoteInferenceHandler.py +311 -0
- edsl/jobs/JobsRemoteInferenceLogger.py +239 -0
- edsl/jobs/RequestTokenEstimator.py +30 -0
- edsl/jobs/async_interview_runner.py +138 -0
- edsl/jobs/buckets/BucketCollection.py +104 -0
- edsl/jobs/buckets/ModelBuckets.py +65 -0
- edsl/jobs/buckets/TokenBucket.py +283 -0
- 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 +392 -0
- edsl/jobs/interviews/InterviewExceptionCollection.py +99 -0
- edsl/jobs/interviews/InterviewExceptionEntry.py +186 -0
- edsl/jobs/interviews/InterviewStatistic.py +63 -0
- edsl/jobs/interviews/InterviewStatisticsCollection.py +25 -0
- edsl/jobs/interviews/InterviewStatusDictionary.py +78 -0
- edsl/jobs/interviews/InterviewStatusLog.py +92 -0
- edsl/jobs/interviews/ReportErrors.py +66 -0
- edsl/jobs/interviews/interview_status_enum.py +9 -0
- 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 -110
- edsl/jobs/runners/JobsRunnerStatus.py +298 -0
- edsl/jobs/tasks/QuestionTaskCreator.py +244 -0
- edsl/jobs/tasks/TaskCreators.py +64 -0
- edsl/jobs/tasks/TaskHistory.py +470 -0
- edsl/jobs/tasks/TaskStatusLog.py +23 -0
- edsl/jobs/tasks/task_status_enum.py +161 -0
- edsl/jobs/tokens/InterviewTokenUsage.py +27 -0
- edsl/jobs/tokens/TokenUsage.py +34 -0
- edsl/language_models/ComputeCost.py +63 -0
- edsl/language_models/LanguageModel.py +507 -386
- edsl/language_models/ModelList.py +164 -0
- edsl/language_models/PriceManager.py +127 -0
- edsl/language_models/RawResponseHandler.py +106 -0
- edsl/language_models/RegisterLanguageModelsMeta.py +184 -0
- edsl/language_models/__init__.py +1 -8
- edsl/language_models/fake_openai_call.py +15 -0
- edsl/language_models/fake_openai_service.py +61 -0
- 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 +109 -41
- edsl/language_models/utilities.py +65 -0
- edsl/notebooks/Notebook.py +263 -0
- edsl/notebooks/NotebookToLaTeX.py +142 -0
- edsl/notebooks/__init__.py +1 -0
- edsl/prompts/Prompt.py +222 -93
- edsl/prompts/__init__.py +1 -1
- edsl/questions/ExceptionExplainer.py +77 -0
- edsl/questions/HTMLQuestion.py +103 -0
- edsl/questions/QuestionBase.py +518 -0
- edsl/questions/QuestionBasePromptsMixin.py +221 -0
- edsl/questions/QuestionBudget.py +164 -67
- edsl/questions/QuestionCheckBox.py +281 -62
- edsl/questions/QuestionDict.py +343 -0
- edsl/questions/QuestionExtract.py +136 -50
- edsl/questions/QuestionFreeText.py +79 -55
- edsl/questions/QuestionFunctional.py +138 -41
- edsl/questions/QuestionList.py +184 -57
- edsl/questions/QuestionMatrix.py +265 -0
- edsl/questions/QuestionMultipleChoice.py +293 -69
- edsl/questions/QuestionNumerical.py +109 -56
- edsl/questions/QuestionRank.py +244 -49
- edsl/questions/Quick.py +41 -0
- edsl/questions/SimpleAskMixin.py +74 -0
- edsl/questions/__init__.py +9 -6
- edsl/questions/{AnswerValidatorMixin.py → answer_validator_mixin.py} +153 -38
- edsl/questions/compose_questions.py +13 -7
- edsl/questions/data_structures.py +20 -0
- edsl/questions/decorators.py +21 -0
- edsl/questions/derived/QuestionLikertFive.py +28 -26
- edsl/questions/derived/QuestionLinearScale.py +41 -28
- edsl/questions/derived/QuestionTopK.py +34 -26
- edsl/questions/derived/QuestionYesNo.py +40 -27
- edsl/questions/descriptors.py +228 -74
- edsl/questions/loop_processor.py +149 -0
- edsl/questions/prompt_templates/question_budget.jinja +13 -0
- edsl/questions/prompt_templates/question_checkbox.jinja +32 -0
- edsl/questions/prompt_templates/question_extract.jinja +11 -0
- edsl/questions/prompt_templates/question_free_text.jinja +3 -0
- edsl/questions/prompt_templates/question_linear_scale.jinja +11 -0
- edsl/questions/prompt_templates/question_list.jinja +17 -0
- edsl/questions/prompt_templates/question_multiple_choice.jinja +33 -0
- edsl/questions/prompt_templates/question_numerical.jinja +37 -0
- edsl/questions/question_base_gen_mixin.py +168 -0
- edsl/questions/question_registry.py +130 -46
- edsl/questions/register_questions_meta.py +71 -0
- edsl/questions/response_validator_abc.py +188 -0
- edsl/questions/response_validator_factory.py +34 -0
- edsl/questions/settings.py +5 -2
- edsl/questions/templates/__init__.py +0 -0
- edsl/questions/templates/budget/__init__.py +0 -0
- edsl/questions/templates/budget/answering_instructions.jinja +7 -0
- edsl/questions/templates/budget/question_presentation.jinja +7 -0
- edsl/questions/templates/checkbox/__init__.py +0 -0
- edsl/questions/templates/checkbox/answering_instructions.jinja +10 -0
- edsl/questions/templates/checkbox/question_presentation.jinja +22 -0
- edsl/questions/templates/dict/__init__.py +0 -0
- edsl/questions/templates/dict/answering_instructions.jinja +21 -0
- edsl/questions/templates/dict/question_presentation.jinja +1 -0
- edsl/questions/templates/extract/__init__.py +0 -0
- edsl/questions/templates/extract/answering_instructions.jinja +7 -0
- edsl/questions/templates/extract/question_presentation.jinja +1 -0
- edsl/questions/templates/free_text/__init__.py +0 -0
- edsl/questions/templates/free_text/answering_instructions.jinja +0 -0
- edsl/questions/templates/free_text/question_presentation.jinja +1 -0
- edsl/questions/templates/likert_five/__init__.py +0 -0
- edsl/questions/templates/likert_five/answering_instructions.jinja +10 -0
- edsl/questions/templates/likert_five/question_presentation.jinja +12 -0
- edsl/questions/templates/linear_scale/__init__.py +0 -0
- edsl/questions/templates/linear_scale/answering_instructions.jinja +5 -0
- edsl/questions/templates/linear_scale/question_presentation.jinja +5 -0
- edsl/questions/templates/list/__init__.py +0 -0
- edsl/questions/templates/list/answering_instructions.jinja +4 -0
- edsl/questions/templates/list/question_presentation.jinja +5 -0
- edsl/questions/templates/matrix/__init__.py +1 -0
- edsl/questions/templates/matrix/answering_instructions.jinja +5 -0
- edsl/questions/templates/matrix/question_presentation.jinja +20 -0
- edsl/questions/templates/multiple_choice/__init__.py +0 -0
- edsl/questions/templates/multiple_choice/answering_instructions.jinja +9 -0
- edsl/questions/templates/multiple_choice/html.jinja +0 -0
- edsl/questions/templates/multiple_choice/question_presentation.jinja +12 -0
- edsl/questions/templates/numerical/__init__.py +0 -0
- edsl/questions/templates/numerical/answering_instructions.jinja +7 -0
- edsl/questions/templates/numerical/question_presentation.jinja +7 -0
- edsl/questions/templates/rank/__init__.py +0 -0
- edsl/questions/templates/rank/answering_instructions.jinja +11 -0
- edsl/questions/templates/rank/question_presentation.jinja +15 -0
- edsl/questions/templates/top_k/__init__.py +0 -0
- edsl/questions/templates/top_k/answering_instructions.jinja +8 -0
- edsl/questions/templates/top_k/question_presentation.jinja +22 -0
- edsl/questions/templates/yes_no/__init__.py +0 -0
- edsl/questions/templates/yes_no/answering_instructions.jinja +6 -0
- edsl/questions/templates/yes_no/question_presentation.jinja +12 -0
- edsl/results/CSSParameterizer.py +108 -0
- edsl/results/Dataset.py +550 -19
- edsl/results/DatasetExportMixin.py +594 -0
- edsl/results/DatasetTree.py +295 -0
- edsl/results/MarkdownToDocx.py +122 -0
- edsl/results/MarkdownToPDF.py +111 -0
- edsl/results/Result.py +477 -173
- edsl/results/Results.py +987 -269
- edsl/results/ResultsExportMixin.py +28 -125
- edsl/results/ResultsGGMixin.py +83 -15
- edsl/results/TableDisplay.py +125 -0
- edsl/results/TextEditor.py +50 -0
- edsl/results/__init__.py +1 -1
- edsl/results/file_exports.py +252 -0
- edsl/results/results_fetch_mixin.py +33 -0
- edsl/results/results_selector.py +145 -0
- edsl/results/results_tools_mixin.py +98 -0
- edsl/results/smart_objects.py +96 -0
- edsl/results/table_data_class.py +12 -0
- edsl/results/table_display.css +78 -0
- edsl/results/table_renderers.py +118 -0
- edsl/results/tree_explore.py +115 -0
- edsl/scenarios/ConstructDownloadLink.py +109 -0
- edsl/scenarios/DocumentChunker.py +102 -0
- edsl/scenarios/DocxScenario.py +16 -0
- edsl/scenarios/FileStore.py +543 -0
- edsl/scenarios/PdfExtractor.py +40 -0
- edsl/scenarios/Scenario.py +431 -62
- edsl/scenarios/ScenarioHtmlMixin.py +65 -0
- edsl/scenarios/ScenarioList.py +1415 -45
- edsl/scenarios/ScenarioListExportMixin.py +45 -0
- edsl/scenarios/ScenarioListPdfMixin.py +239 -0
- edsl/scenarios/__init__.py +2 -0
- edsl/scenarios/directory_scanner.py +96 -0
- edsl/scenarios/file_methods.py +85 -0
- edsl/scenarios/handlers/__init__.py +13 -0
- edsl/scenarios/handlers/csv.py +49 -0
- edsl/scenarios/handlers/docx.py +76 -0
- edsl/scenarios/handlers/html.py +37 -0
- edsl/scenarios/handlers/json.py +111 -0
- edsl/scenarios/handlers/latex.py +5 -0
- edsl/scenarios/handlers/md.py +51 -0
- edsl/scenarios/handlers/pdf.py +68 -0
- edsl/scenarios/handlers/png.py +39 -0
- edsl/scenarios/handlers/pptx.py +105 -0
- edsl/scenarios/handlers/py.py +294 -0
- edsl/scenarios/handlers/sql.py +313 -0
- edsl/scenarios/handlers/sqlite.py +149 -0
- edsl/scenarios/handlers/txt.py +33 -0
- edsl/scenarios/scenario_join.py +131 -0
- edsl/scenarios/scenario_selector.py +156 -0
- edsl/shared.py +1 -0
- edsl/study/ObjectEntry.py +173 -0
- edsl/study/ProofOfWork.py +113 -0
- edsl/study/SnapShot.py +80 -0
- edsl/study/Study.py +521 -0
- edsl/study/__init__.py +4 -0
- edsl/surveys/ConstructDAG.py +92 -0
- edsl/surveys/DAG.py +92 -11
- edsl/surveys/EditSurvey.py +221 -0
- edsl/surveys/InstructionHandler.py +100 -0
- edsl/surveys/Memory.py +9 -4
- edsl/surveys/MemoryManagement.py +72 -0
- edsl/surveys/MemoryPlan.py +156 -35
- edsl/surveys/Rule.py +221 -74
- edsl/surveys/RuleCollection.py +241 -61
- edsl/surveys/RuleManager.py +172 -0
- edsl/surveys/Simulator.py +75 -0
- edsl/surveys/Survey.py +1079 -339
- edsl/surveys/SurveyCSS.py +273 -0
- edsl/surveys/SurveyExportMixin.py +235 -40
- edsl/surveys/SurveyFlowVisualization.py +181 -0
- edsl/surveys/SurveyQualtricsImport.py +284 -0
- edsl/surveys/SurveyToApp.py +141 -0
- edsl/surveys/__init__.py +4 -2
- edsl/surveys/base.py +19 -3
- edsl/surveys/descriptors.py +17 -6
- edsl/surveys/instructions/ChangeInstruction.py +48 -0
- edsl/surveys/instructions/Instruction.py +56 -0
- edsl/surveys/instructions/InstructionCollection.py +82 -0
- edsl/surveys/instructions/__init__.py +0 -0
- edsl/templates/error_reporting/base.html +24 -0
- edsl/templates/error_reporting/exceptions_by_model.html +35 -0
- edsl/templates/error_reporting/exceptions_by_question_name.html +17 -0
- edsl/templates/error_reporting/exceptions_by_type.html +17 -0
- edsl/templates/error_reporting/interview_details.html +116 -0
- edsl/templates/error_reporting/interviews.html +19 -0
- edsl/templates/error_reporting/overview.html +5 -0
- edsl/templates/error_reporting/performance_plot.html +2 -0
- edsl/templates/error_reporting/report.css +74 -0
- edsl/templates/error_reporting/report.html +118 -0
- edsl/templates/error_reporting/report.js +25 -0
- edsl/tools/__init__.py +1 -0
- edsl/tools/clusters.py +192 -0
- edsl/tools/embeddings.py +27 -0
- edsl/tools/embeddings_plotting.py +118 -0
- edsl/tools/plotting.py +112 -0
- edsl/tools/summarize.py +18 -0
- edsl/utilities/PrettyList.py +56 -0
- edsl/utilities/SystemInfo.py +5 -0
- edsl/utilities/__init__.py +21 -20
- edsl/utilities/ast_utilities.py +3 -0
- edsl/utilities/data/Registry.py +2 -0
- edsl/utilities/decorators.py +41 -0
- edsl/utilities/gcp_bucket/__init__.py +0 -0
- edsl/utilities/gcp_bucket/cloud_storage.py +96 -0
- edsl/utilities/interface.py +310 -60
- edsl/utilities/is_notebook.py +18 -0
- edsl/utilities/is_valid_variable_name.py +11 -0
- edsl/utilities/naming_utilities.py +263 -0
- edsl/utilities/remove_edsl_version.py +24 -0
- edsl/utilities/repair_functions.py +28 -0
- edsl/utilities/restricted_python.py +70 -0
- edsl/utilities/utilities.py +203 -13
- edsl-0.1.40.dist-info/METADATA +111 -0
- edsl-0.1.40.dist-info/RECORD +362 -0
- {edsl-0.1.15.dist-info → edsl-0.1.40.dist-info}/WHEEL +1 -1
- edsl/agents/AgentListExportMixin.py +0 -24
- edsl/coop/old.py +0 -31
- edsl/data/Database.py +0 -141
- edsl/data/crud.py +0 -121
- edsl/jobs/Interview.py +0 -435
- edsl/jobs/JobsRunner.py +0 -63
- edsl/jobs/JobsRunnerStatusMixin.py +0 -115
- edsl/jobs/base.py +0 -47
- edsl/jobs/buckets.py +0 -178
- edsl/jobs/runners/JobsRunnerDryRun.py +0 -19
- edsl/jobs/runners/JobsRunnerStreaming.py +0 -54
- edsl/jobs/task_management.py +0 -215
- edsl/jobs/token_tracking.py +0 -78
- edsl/language_models/DeepInfra.py +0 -69
- edsl/language_models/OpenAI.py +0 -98
- edsl/language_models/model_interfaces/GeminiPro.py +0 -66
- edsl/language_models/model_interfaces/LanguageModelOpenAIFour.py +0 -8
- edsl/language_models/model_interfaces/LanguageModelOpenAIThreeFiveTurbo.py +0 -8
- edsl/language_models/model_interfaces/LlamaTwo13B.py +0 -21
- edsl/language_models/model_interfaces/LlamaTwo70B.py +0 -21
- edsl/language_models/model_interfaces/Mixtral8x7B.py +0 -24
- edsl/language_models/registry.py +0 -81
- edsl/language_models/schemas.py +0 -15
- edsl/language_models/unused/ReplicateBase.py +0 -83
- edsl/prompts/QuestionInstructionsBase.py +0 -6
- edsl/prompts/library/agent_instructions.py +0 -29
- edsl/prompts/library/agent_persona.py +0 -17
- edsl/prompts/library/question_budget.py +0 -26
- edsl/prompts/library/question_checkbox.py +0 -32
- edsl/prompts/library/question_extract.py +0 -19
- edsl/prompts/library/question_freetext.py +0 -14
- edsl/prompts/library/question_linear_scale.py +0 -20
- edsl/prompts/library/question_list.py +0 -22
- edsl/prompts/library/question_multiple_choice.py +0 -44
- edsl/prompts/library/question_numerical.py +0 -31
- edsl/prompts/library/question_rank.py +0 -21
- edsl/prompts/prompt_config.py +0 -33
- edsl/prompts/registry.py +0 -185
- edsl/questions/Question.py +0 -240
- edsl/report/InputOutputDataTypes.py +0 -134
- edsl/report/RegressionMixin.py +0 -28
- edsl/report/ReportOutputs.py +0 -1228
- edsl/report/ResultsFetchMixin.py +0 -106
- edsl/report/ResultsOutputMixin.py +0 -14
- edsl/report/demo.ipynb +0 -645
- edsl/results/ResultsDBMixin.py +0 -184
- edsl/surveys/SurveyFlowVisualizationMixin.py +0 -92
- edsl/trackers/Tracker.py +0 -91
- edsl/trackers/TrackerAPI.py +0 -196
- edsl/trackers/TrackerTasks.py +0 -70
- edsl/utilities/pastebin.py +0 -141
- edsl-0.1.15.dist-info/METADATA +0 -69
- edsl-0.1.15.dist-info/RECORD +0 -142
- /edsl/{language_models/model_interfaces → inference_services}/__init__.py +0 -0
- /edsl/{report/__init__.py → jobs/runners/JobsRunnerStatusData.py} +0 -0
- /edsl/{trackers/__init__.py → language_models/ServiceDataSources.py} +0 -0
- {edsl-0.1.15.dist-info → edsl-0.1.40.dist-info}/LICENSE +0 -0
@@ -0,0 +1,343 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
from typing import Union, Optional, Dict, List, Any, Type
|
3
|
+
from pydantic import BaseModel, Field, field_validator
|
4
|
+
from jinja2 import Environment, FileSystemLoader, TemplateNotFound
|
5
|
+
from pathlib import Path
|
6
|
+
|
7
|
+
from edsl.questions.QuestionBase import QuestionBase
|
8
|
+
from edsl.questions.descriptors import (
|
9
|
+
AnswerKeysDescriptor,
|
10
|
+
ValueTypesDescriptor,
|
11
|
+
ValueDescriptionsDescriptor,
|
12
|
+
QuestionTextDescriptor,
|
13
|
+
)
|
14
|
+
from edsl.questions.response_validator_abc import ResponseValidatorABC
|
15
|
+
from edsl.exceptions.questions import QuestionCreationValidationError
|
16
|
+
from edsl.questions.decorators import inject_exception
|
17
|
+
|
18
|
+
|
19
|
+
class DictResponseValidator(ResponseValidatorABC):
|
20
|
+
required_params = ["answer_keys", "permissive"]
|
21
|
+
|
22
|
+
valid_examples = [
|
23
|
+
(
|
24
|
+
{
|
25
|
+
"answer": {
|
26
|
+
"name": "Hot Chocolate",
|
27
|
+
"num_ingredients": 5,
|
28
|
+
"ingredients": ["milk", "cocoa", "sugar"]
|
29
|
+
}
|
30
|
+
},
|
31
|
+
{
|
32
|
+
"answer_keys": ["name", "num_ingredients", "ingredients"],
|
33
|
+
"value_types": ["str", "int", "list[str]"]
|
34
|
+
},
|
35
|
+
)
|
36
|
+
]
|
37
|
+
invalid_examples = [
|
38
|
+
(
|
39
|
+
{"answer": {"name": 123}}, # Name should be a string
|
40
|
+
{"answer_keys": ["name"], "value_types": ["str"]},
|
41
|
+
"Key 'name' has value of type int, expected str",
|
42
|
+
),
|
43
|
+
(
|
44
|
+
{"answer": {"ingredients": "milk"}}, # Should be a list
|
45
|
+
{"answer_keys": ["ingredients"], "value_types": ["list"]},
|
46
|
+
"Key 'ingredients' should be a list, got str",
|
47
|
+
)
|
48
|
+
]
|
49
|
+
|
50
|
+
|
51
|
+
class QuestionDict(QuestionBase):
|
52
|
+
question_type = "dict"
|
53
|
+
question_text: str = QuestionTextDescriptor()
|
54
|
+
answer_keys: List[str] = AnswerKeysDescriptor()
|
55
|
+
value_types: Optional[List[str]] = ValueTypesDescriptor()
|
56
|
+
value_descriptions: Optional[List[str]] = ValueDescriptionsDescriptor()
|
57
|
+
|
58
|
+
_response_model = None
|
59
|
+
response_validator_class = DictResponseValidator
|
60
|
+
|
61
|
+
def _get_default_answer(self) -> Dict[str, Any]:
|
62
|
+
"""Get default answer based on types."""
|
63
|
+
answer = {}
|
64
|
+
if not self.value_types:
|
65
|
+
return {
|
66
|
+
"title": "Sample Recipe",
|
67
|
+
"ingredients": ["ingredient1", "ingredient2"],
|
68
|
+
"num_ingredients": 2,
|
69
|
+
"instructions": "Sample instructions"
|
70
|
+
}
|
71
|
+
|
72
|
+
for key, type_str in zip(self.answer_keys, self.value_types):
|
73
|
+
if type_str.startswith(('list[', 'list')):
|
74
|
+
if '[' in type_str:
|
75
|
+
element_type = type_str[type_str.index('[') + 1:type_str.rindex(']')].lower()
|
76
|
+
if element_type == 'str':
|
77
|
+
answer[key] = ["sample_string"]
|
78
|
+
elif element_type == 'int':
|
79
|
+
answer[key] = [1]
|
80
|
+
elif element_type == 'float':
|
81
|
+
answer[key] = [1.0]
|
82
|
+
else:
|
83
|
+
answer[key] = []
|
84
|
+
else:
|
85
|
+
answer[key] = []
|
86
|
+
else:
|
87
|
+
if type_str == 'str':
|
88
|
+
answer[key] = "sample_string"
|
89
|
+
elif type_str == 'int':
|
90
|
+
answer[key] = 1
|
91
|
+
elif type_str == 'float':
|
92
|
+
answer[key] = 1.0
|
93
|
+
else:
|
94
|
+
answer[key] = None
|
95
|
+
|
96
|
+
return answer
|
97
|
+
|
98
|
+
def create_response_model(
|
99
|
+
self,
|
100
|
+
) -> Type[BaseModel]:
|
101
|
+
"""Create a response model for dict questions."""
|
102
|
+
default_answer = self._get_default_answer()
|
103
|
+
|
104
|
+
class DictResponse(BaseModel):
|
105
|
+
answer: Dict[str, Any] = Field(
|
106
|
+
default_factory=lambda: default_answer.copy()
|
107
|
+
)
|
108
|
+
comment: Optional[str] = None
|
109
|
+
|
110
|
+
@field_validator("answer")
|
111
|
+
def validate_answer(cls, v, values, **kwargs):
|
112
|
+
# Ensure all keys exist
|
113
|
+
missing_keys = set(self.answer_keys) - set(v.keys())
|
114
|
+
if missing_keys:
|
115
|
+
raise ValueError(f"Missing required keys: {missing_keys}")
|
116
|
+
|
117
|
+
# Validate value types if not permissive
|
118
|
+
if not self.permissive and self.value_types:
|
119
|
+
for key, type_str in zip(self.answer_keys, self.value_types):
|
120
|
+
if key not in v:
|
121
|
+
continue
|
122
|
+
|
123
|
+
value = v[key]
|
124
|
+
type_str = type_str.lower() # Normalize to lowercase
|
125
|
+
|
126
|
+
# Handle list types
|
127
|
+
if type_str.startswith(('list[', 'list')):
|
128
|
+
if not isinstance(value, list):
|
129
|
+
raise ValueError(f"Key '{key}' should be a list, got {type(value).__name__}")
|
130
|
+
|
131
|
+
# If it's a parameterized list, check element types
|
132
|
+
if '[' in type_str:
|
133
|
+
element_type = type_str[type_str.index('[') + 1:type_str.rindex(']')]
|
134
|
+
element_type = element_type.lower().strip()
|
135
|
+
|
136
|
+
for i, elem in enumerate(value):
|
137
|
+
expected_type = {
|
138
|
+
'str': str,
|
139
|
+
'int': int,
|
140
|
+
'float': float,
|
141
|
+
'list': list
|
142
|
+
}.get(element_type)
|
143
|
+
|
144
|
+
if expected_type and not isinstance(elem, expected_type):
|
145
|
+
raise ValueError(
|
146
|
+
f"List element at index {i} for key '{key}' "
|
147
|
+
f"has type {type(elem).__name__}, expected {element_type}"
|
148
|
+
)
|
149
|
+
else:
|
150
|
+
# Handle basic types
|
151
|
+
expected_type = {
|
152
|
+
'str': str,
|
153
|
+
'int': int,
|
154
|
+
'float': float,
|
155
|
+
'list': list,
|
156
|
+
}.get(type_str)
|
157
|
+
|
158
|
+
if expected_type and not isinstance(value, expected_type):
|
159
|
+
raise ValueError(
|
160
|
+
f"Key '{key}' has value of type {type(value).__name__}, expected {type_str}"
|
161
|
+
)
|
162
|
+
return v
|
163
|
+
|
164
|
+
model_config = {
|
165
|
+
"json_schema_extra": {
|
166
|
+
"examples": [{
|
167
|
+
"answer": default_answer,
|
168
|
+
"comment": None
|
169
|
+
}]
|
170
|
+
}
|
171
|
+
}
|
172
|
+
|
173
|
+
DictResponse.__name__ = "DictResponse"
|
174
|
+
return DictResponse
|
175
|
+
|
176
|
+
def __init__(
|
177
|
+
self,
|
178
|
+
question_name: str,
|
179
|
+
question_text: str,
|
180
|
+
answer_keys: List[str],
|
181
|
+
value_types: Optional[List[Union[str, type]]] = None,
|
182
|
+
value_descriptions: Optional[List[str]] = None,
|
183
|
+
include_comment: bool = True,
|
184
|
+
question_presentation: Optional[str] = None,
|
185
|
+
answering_instructions: Optional[str] = None,
|
186
|
+
permissive: bool = False,
|
187
|
+
):
|
188
|
+
self.question_name = question_name
|
189
|
+
self.question_text = question_text
|
190
|
+
self.answer_keys = answer_keys
|
191
|
+
self.value_types = self._normalize_value_types(value_types)
|
192
|
+
self.value_descriptions = value_descriptions
|
193
|
+
self.include_comment = include_comment
|
194
|
+
self.question_presentation = question_presentation or self._render_template(
|
195
|
+
"question_presentation.jinja"
|
196
|
+
)
|
197
|
+
self.answering_instructions = answering_instructions or self._render_template(
|
198
|
+
"answering_instructions.jinja"
|
199
|
+
)
|
200
|
+
self.permissive = permissive
|
201
|
+
|
202
|
+
# Validation
|
203
|
+
if self.value_types and len(self.value_types) != len(self.answer_keys):
|
204
|
+
raise QuestionCreationValidationError(
|
205
|
+
"Length of value_types must match length of answer_keys."
|
206
|
+
)
|
207
|
+
if self.value_descriptions and len(self.value_descriptions) != len(self.answer_keys):
|
208
|
+
raise QuestionCreationValidationError(
|
209
|
+
"Length of value_descriptions must match length of answer_keys."
|
210
|
+
)
|
211
|
+
|
212
|
+
@staticmethod
|
213
|
+
def _normalize_value_types(value_types: Optional[List[Union[str, type]]]) -> Optional[List[str]]:
|
214
|
+
"""Convert all value_types to string representations, including type hints."""
|
215
|
+
if not value_types:
|
216
|
+
return None
|
217
|
+
|
218
|
+
def normalize_type(t) -> str:
|
219
|
+
# Handle string representations of List
|
220
|
+
t_str = str(t)
|
221
|
+
if t_str == 'List':
|
222
|
+
return 'list'
|
223
|
+
|
224
|
+
# Handle string inputs
|
225
|
+
if isinstance(t, str):
|
226
|
+
t = t.lower()
|
227
|
+
# Handle list types
|
228
|
+
if t.startswith(('list[', 'list')):
|
229
|
+
if '[' in t:
|
230
|
+
# Normalize the inner type
|
231
|
+
inner_type = t[t.index('[') + 1:t.rindex(']')].strip().lower()
|
232
|
+
return f"list[{inner_type}]"
|
233
|
+
return "list"
|
234
|
+
return t
|
235
|
+
|
236
|
+
# Handle List the same as list
|
237
|
+
if t_str == "<class 'List'>":
|
238
|
+
return "list"
|
239
|
+
|
240
|
+
# If it's list type
|
241
|
+
if t is list:
|
242
|
+
return "list"
|
243
|
+
|
244
|
+
# If it's a basic type
|
245
|
+
if hasattr(t, "__name__"):
|
246
|
+
return t.__name__.lower()
|
247
|
+
|
248
|
+
# If it's a typing.List
|
249
|
+
if t_str.startswith(('list[', 'list')):
|
250
|
+
return t_str.replace('typing.', '').lower()
|
251
|
+
|
252
|
+
# Handle generic types
|
253
|
+
if hasattr(t, "__origin__"):
|
254
|
+
origin = t.__origin__.__name__.lower()
|
255
|
+
args = [
|
256
|
+
arg.__name__.lower() if hasattr(arg, "__name__") else str(arg).lower()
|
257
|
+
for arg in t.__args__
|
258
|
+
]
|
259
|
+
return f"{origin}[{', '.join(args)}]"
|
260
|
+
|
261
|
+
raise QuestionCreationValidationError(
|
262
|
+
f"Invalid type in value_types: {t}. Must be a type or string."
|
263
|
+
)
|
264
|
+
|
265
|
+
normalized = []
|
266
|
+
for t in value_types:
|
267
|
+
try:
|
268
|
+
normalized.append(normalize_type(t))
|
269
|
+
except Exception as e:
|
270
|
+
raise QuestionCreationValidationError(f"Error normalizing type {t}: {str(e)}")
|
271
|
+
|
272
|
+
return normalized
|
273
|
+
|
274
|
+
def _render_template(self, template_name: str) -> str:
|
275
|
+
"""Render a template using Jinja."""
|
276
|
+
try:
|
277
|
+
template_dir = Path(__file__).parent / "templates" / "dict"
|
278
|
+
env = Environment(loader=FileSystemLoader(template_dir))
|
279
|
+
template = env.get_template(template_name)
|
280
|
+
return template.render(
|
281
|
+
question_name=self.question_name,
|
282
|
+
question_text=self.question_text,
|
283
|
+
answer_keys=self.answer_keys,
|
284
|
+
value_types=self.value_types,
|
285
|
+
value_descriptions=self.value_descriptions,
|
286
|
+
include_comment=self.include_comment,
|
287
|
+
)
|
288
|
+
except TemplateNotFound:
|
289
|
+
return f"Template {template_name} not found in {template_dir}."
|
290
|
+
|
291
|
+
def to_dict(self, add_edsl_version: bool = True) -> dict:
|
292
|
+
"""Serialize to JSON-compatible dictionary."""
|
293
|
+
return {
|
294
|
+
"question_type": self.question_type,
|
295
|
+
"question_name": self.question_name,
|
296
|
+
"question_text": self.question_text,
|
297
|
+
"answer_keys": self.answer_keys,
|
298
|
+
"value_types": self.value_types or [],
|
299
|
+
"value_descriptions": self.value_descriptions or [],
|
300
|
+
"include_comment": self.include_comment,
|
301
|
+
"permissive": self.permissive,
|
302
|
+
}
|
303
|
+
|
304
|
+
@classmethod
|
305
|
+
def from_dict(cls, data: dict) -> 'QuestionDict':
|
306
|
+
"""Recreate from a dictionary."""
|
307
|
+
return cls(
|
308
|
+
question_name=data["question_name"],
|
309
|
+
question_text=data["question_text"],
|
310
|
+
answer_keys=data["answer_keys"],
|
311
|
+
value_types=data.get("value_types"),
|
312
|
+
value_descriptions=data.get("value_descriptions"),
|
313
|
+
include_comment=data.get("include_comment", True),
|
314
|
+
permissive=data.get("permissive", False),
|
315
|
+
)
|
316
|
+
|
317
|
+
@classmethod
|
318
|
+
@inject_exception
|
319
|
+
def example(cls) -> 'QuestionDict':
|
320
|
+
"""Return an example question."""
|
321
|
+
return cls(
|
322
|
+
question_name="example",
|
323
|
+
question_text="Please provide a simple recipe for hot chocolate.",
|
324
|
+
answer_keys=["title", "ingredients", "num_ingredients", "instructions"],
|
325
|
+
value_types=["str", "list[str]", "int", "str"],
|
326
|
+
value_descriptions=[
|
327
|
+
"The title of the recipe.",
|
328
|
+
"A list of ingredients.",
|
329
|
+
"The number of ingredients.",
|
330
|
+
"The instructions for making the recipe."
|
331
|
+
],
|
332
|
+
)
|
333
|
+
|
334
|
+
def _simulate_answer(self) -> dict:
|
335
|
+
"""Simulate an answer for the question."""
|
336
|
+
return {
|
337
|
+
"answer": self._get_default_answer(),
|
338
|
+
"comment": None
|
339
|
+
}
|
340
|
+
|
341
|
+
if __name__ == "__main__":
|
342
|
+
q = QuestionDict.example()
|
343
|
+
print(q.to_dict())
|
@@ -1,71 +1,152 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
-
import re
|
3
2
|
import json
|
4
|
-
|
5
|
-
|
3
|
+
import re
|
4
|
+
|
5
|
+
from typing import Any, Optional, Dict
|
6
|
+
from edsl.questions.QuestionBase import QuestionBase
|
6
7
|
from edsl.questions.descriptors import AnswerTemplateDescriptor
|
7
|
-
from edsl.scenarios import Scenario
|
8
|
-
from edsl.utilities import random_string
|
9
8
|
|
9
|
+
from edsl.questions.response_validator_abc import ResponseValidatorABC
|
10
|
+
from edsl.questions.data_structures import BaseResponse
|
11
|
+
from edsl.questions.decorators import inject_exception
|
12
|
+
|
13
|
+
from typing import Dict, Any
|
14
|
+
from pydantic import create_model, Field
|
15
|
+
|
16
|
+
|
17
|
+
def extract_json(text, expected_keys, verbose=False):
|
18
|
+
# Escape special regex characters in keys
|
19
|
+
escaped_keys = [re.escape(key) for key in expected_keys]
|
20
|
+
|
21
|
+
# Create a pattern that looks for all expected keys
|
22
|
+
pattern = r"\{[^}]*" + r"[^}]*".join(escaped_keys) + r"[^}]*\}"
|
23
|
+
|
24
|
+
json_match = re.search(pattern, text)
|
25
|
+
|
26
|
+
if json_match:
|
27
|
+
json_str = json_match.group(0)
|
28
|
+
try:
|
29
|
+
# Parse the extracted string as JSON
|
30
|
+
json_data = json.loads(json_str)
|
31
|
+
|
32
|
+
# Verify that all expected keys are present
|
33
|
+
if all(key in json_data for key in expected_keys):
|
34
|
+
return json_data
|
35
|
+
else:
|
36
|
+
if verbose:
|
37
|
+
print(
|
38
|
+
"Error: Not all expected keys were found in the extracted JSON."
|
39
|
+
)
|
40
|
+
return None
|
41
|
+
except json.JSONDecodeError:
|
42
|
+
if verbose:
|
43
|
+
print("Error: The extracted content is not valid JSON.")
|
44
|
+
return None
|
45
|
+
else:
|
46
|
+
if verbose:
|
47
|
+
print("Error: No JSON-like structure found with all expected keys.")
|
48
|
+
return None
|
49
|
+
|
50
|
+
|
51
|
+
def dict_to_pydantic_model(input_dict: Dict[str, Any]) -> Any:
|
52
|
+
field_definitions = {
|
53
|
+
key: (type(value), Field(default=value)) for key, value in input_dict.items()
|
54
|
+
}
|
55
|
+
|
56
|
+
DynamicModel = create_model("DynamicModel", **field_definitions)
|
57
|
+
|
58
|
+
class AnswerModel(BaseResponse):
|
59
|
+
answer: "DynamicModel"
|
60
|
+
generated_tokens: Optional[str] = None
|
61
|
+
comment: Optional[str] = None
|
10
62
|
|
11
|
-
|
12
|
-
"""
|
13
|
-
This question asks the user to extract values from a string, and return them in a given template.
|
63
|
+
return AnswerModel
|
14
64
|
|
15
|
-
Arguments:
|
16
|
-
- `question_name` is the name of the question (string)
|
17
|
-
- `question_text` is the text of the question (string)
|
18
|
-
- `answer_template` is the template for the answer (dictionary mapping strings to strings)
|
19
65
|
|
20
|
-
|
21
|
-
|
66
|
+
class ExtractResponseValidator(ResponseValidatorABC):
|
67
|
+
required_params = ["answer_template"]
|
68
|
+
valid_examples = [({"answer": "This is great"}, {})]
|
69
|
+
invalid_examples = [
|
70
|
+
(
|
71
|
+
{"answer": None},
|
72
|
+
{"answer_template": {"name": "John Doe", "profession": "Carpenter"}},
|
73
|
+
"Result cannot be empty",
|
74
|
+
),
|
75
|
+
]
|
22
76
|
|
23
|
-
|
24
|
-
|
77
|
+
def custom_validate(self, response) -> BaseResponse:
|
78
|
+
return response.dict()
|
79
|
+
|
80
|
+
def fix(self, response, verbose=False):
|
81
|
+
raw_tokens = response["generated_tokens"]
|
82
|
+
if verbose:
|
83
|
+
print(f"Invalid response of QuestionExtract was: {raw_tokens}")
|
84
|
+
extracted_json = extract_json(raw_tokens, self.answer_template.keys(), verbose)
|
85
|
+
if verbose:
|
86
|
+
print("Proposed solution is: ", extracted_json)
|
87
|
+
return {
|
88
|
+
"answer": extracted_json,
|
89
|
+
"comment": response.get("comment", None),
|
90
|
+
"generated_tokens": raw_tokens,
|
91
|
+
}
|
92
|
+
|
93
|
+
|
94
|
+
class QuestionExtract(QuestionBase):
|
95
|
+
"""This question prompts the agent to extract information from a string and return it in a given template."""
|
25
96
|
|
26
97
|
question_type = "extract"
|
27
98
|
answer_template: dict[str, Any] = AnswerTemplateDescriptor()
|
99
|
+
_response_model = None
|
100
|
+
response_validator_class = ExtractResponseValidator
|
28
101
|
|
29
102
|
def __init__(
|
30
103
|
self,
|
31
104
|
question_text: str,
|
32
105
|
answer_template: dict[str, Any],
|
33
106
|
question_name: str,
|
107
|
+
answering_instructions: str = None,
|
108
|
+
question_presentation: str = None,
|
34
109
|
):
|
110
|
+
"""Initialize the question.
|
111
|
+
|
112
|
+
:param question_name: The name of the question.
|
113
|
+
:param question_text: The text of the question.
|
114
|
+
:param answer_template: The template for the answer.
|
115
|
+
:param answering_instructions: Instructions for answering the question.
|
116
|
+
:param question_presentation: The presentation of the question.
|
117
|
+
"""
|
35
118
|
self.question_name = question_name
|
36
119
|
self.question_text = question_text
|
37
120
|
self.answer_template = answer_template
|
121
|
+
self.answering_instructions = answering_instructions
|
122
|
+
self.question_presentation = question_presentation
|
38
123
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
"""
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
return
|
60
|
-
"answer": {key: random_string() for key in self.answer_template.keys()},
|
61
|
-
"comment": random_string(),
|
62
|
-
}
|
124
|
+
def create_response_model(self):
|
125
|
+
return dict_to_pydantic_model(self.answer_template)
|
126
|
+
|
127
|
+
@property
|
128
|
+
def question_html_content(self) -> str:
|
129
|
+
from jinja2 import Template
|
130
|
+
|
131
|
+
question_html_content = Template(
|
132
|
+
"""
|
133
|
+
{% for field, placeholder in answer_template.items() %}
|
134
|
+
<div>
|
135
|
+
<label for="{{ field }}">{{ field }}</label>
|
136
|
+
<input type="text" id="{{ field }}" name="{{ question_name }}[{{ field }}]" placeholder="{{ placeholder }}">
|
137
|
+
</div>
|
138
|
+
{% endfor %}
|
139
|
+
"""
|
140
|
+
).render(
|
141
|
+
question_name=self.question_name,
|
142
|
+
answer_template=self.answer_template,
|
143
|
+
)
|
144
|
+
return question_html_content
|
63
145
|
|
64
|
-
################
|
65
|
-
# Helpful methods
|
66
|
-
################
|
67
146
|
@classmethod
|
147
|
+
@inject_exception
|
68
148
|
def example(cls) -> QuestionExtract:
|
149
|
+
"""Return an example question."""
|
69
150
|
return cls(
|
70
151
|
question_name="extract_name",
|
71
152
|
question_text="My name is Moby Dick. I have a PhD in astrology, but I'm actually a truck driver",
|
@@ -73,22 +154,27 @@ class QuestionExtract(Question):
|
|
73
154
|
)
|
74
155
|
|
75
156
|
|
76
|
-
# main
|
77
157
|
def main():
|
158
|
+
"""Administer a question and validate the answer."""
|
78
159
|
from edsl.questions.QuestionExtract import QuestionExtract
|
79
160
|
|
80
161
|
q = QuestionExtract.example()
|
81
162
|
q.question_text
|
82
163
|
q.question_name
|
83
164
|
q.answer_template
|
84
|
-
q.
|
85
|
-
q.
|
165
|
+
q._validate_answer({"answer": {"name": "Moby", "profession": "truck driver"}})
|
166
|
+
q._translate_answer_code_to_answer(
|
86
167
|
{"answer": {"name": "Moby", "profession": "truck driver"}}
|
87
168
|
)
|
88
|
-
|
89
|
-
q.
|
90
|
-
q.
|
91
|
-
q.validate_answer(q.simulate_answer(human_readable=False))
|
169
|
+
q._simulate_answer()
|
170
|
+
q._simulate_answer(human_readable=False)
|
171
|
+
q._validate_answer(q._simulate_answer(human_readable=False))
|
92
172
|
# serialization (inherits from Question)
|
93
173
|
q.to_dict()
|
94
174
|
assert q.from_dict(q.to_dict()) == q
|
175
|
+
|
176
|
+
|
177
|
+
if __name__ == "__main__":
|
178
|
+
import doctest
|
179
|
+
|
180
|
+
doctest.testmod(optionflags=doctest.ELLIPSIS)
|