edsl 0.1.47__py3-none-any.whl → 0.1.48__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 +44 -39
- edsl/__version__.py +1 -1
- edsl/agents/__init__.py +4 -2
- edsl/agents/{Agent.py → agent.py} +442 -152
- edsl/agents/{AgentList.py → agent_list.py} +220 -162
- edsl/agents/descriptors.py +46 -7
- edsl/{exceptions/agents.py → agents/exceptions.py} +3 -12
- edsl/base/__init__.py +75 -0
- edsl/base/base_class.py +1303 -0
- edsl/base/data_transfer_models.py +114 -0
- edsl/base/enums.py +215 -0
- edsl/base.py +8 -0
- edsl/buckets/__init__.py +25 -0
- edsl/buckets/bucket_collection.py +324 -0
- edsl/buckets/model_buckets.py +206 -0
- edsl/buckets/token_bucket.py +502 -0
- edsl/{jobs/buckets/TokenBucketAPI.py → buckets/token_bucket_api.py} +1 -1
- edsl/buckets/token_bucket_client.py +509 -0
- edsl/caching/__init__.py +20 -0
- edsl/caching/cache.py +814 -0
- edsl/caching/cache_entry.py +427 -0
- edsl/{data/CacheHandler.py → caching/cache_handler.py} +14 -15
- edsl/caching/exceptions.py +24 -0
- edsl/caching/orm.py +30 -0
- edsl/{data/RemoteCacheSync.py → caching/remote_cache_sync.py} +3 -3
- edsl/caching/sql_dict.py +441 -0
- edsl/config/__init__.py +8 -0
- edsl/config/config_class.py +177 -0
- edsl/config.py +4 -176
- edsl/conversation/Conversation.py +7 -7
- edsl/conversation/car_buying.py +4 -4
- edsl/conversation/chips.py +6 -6
- edsl/coop/__init__.py +25 -2
- edsl/coop/coop.py +303 -67
- edsl/coop/{ExpectedParrotKeyHandler.py → ep_key_handling.py} +86 -10
- edsl/coop/exceptions.py +62 -0
- edsl/coop/price_fetcher.py +126 -0
- edsl/coop/utils.py +89 -24
- edsl/data_transfer_models.py +5 -72
- edsl/dataset/__init__.py +10 -0
- edsl/{results/Dataset.py → dataset/dataset.py} +116 -36
- edsl/{results/DatasetExportMixin.py → dataset/dataset_operations_mixin.py} +606 -122
- edsl/{results/DatasetTree.py → dataset/dataset_tree.py} +156 -75
- edsl/{results/TableDisplay.py → dataset/display/table_display.py} +18 -7
- edsl/{results → dataset/display}/table_renderers.py +58 -2
- edsl/{results → dataset}/file_exports.py +4 -5
- edsl/{results → dataset}/smart_objects.py +2 -2
- edsl/enums.py +5 -205
- edsl/inference_services/__init__.py +5 -0
- edsl/inference_services/{AvailableModelCacheHandler.py → available_model_cache_handler.py} +2 -3
- edsl/inference_services/{AvailableModelFetcher.py → available_model_fetcher.py} +8 -14
- edsl/inference_services/data_structures.py +3 -2
- edsl/{exceptions/inference_services.py → inference_services/exceptions.py} +1 -1
- edsl/inference_services/{InferenceServiceABC.py → inference_service_abc.py} +1 -1
- edsl/inference_services/{InferenceServicesCollection.py → inference_services_collection.py} +8 -7
- edsl/inference_services/registry.py +4 -41
- edsl/inference_services/{ServiceAvailability.py → service_availability.py} +5 -25
- edsl/inference_services/services/__init__.py +31 -0
- edsl/inference_services/{AnthropicService.py → services/anthropic_service.py} +3 -3
- edsl/inference_services/{AwsBedrock.py → services/aws_bedrock.py} +2 -2
- edsl/inference_services/{AzureAI.py → services/azure_ai.py} +2 -2
- edsl/inference_services/{DeepInfraService.py → services/deep_infra_service.py} +1 -3
- edsl/inference_services/{DeepSeekService.py → services/deep_seek_service.py} +2 -4
- edsl/inference_services/{GoogleService.py → services/google_service.py} +5 -4
- edsl/inference_services/{GroqService.py → services/groq_service.py} +1 -1
- edsl/inference_services/{MistralAIService.py → services/mistral_ai_service.py} +3 -3
- edsl/inference_services/{OllamaService.py → services/ollama_service.py} +1 -7
- edsl/inference_services/{OpenAIService.py → services/open_ai_service.py} +5 -6
- edsl/inference_services/{PerplexityService.py → services/perplexity_service.py} +3 -7
- edsl/inference_services/{TestService.py → services/test_service.py} +7 -6
- edsl/inference_services/{TogetherAIService.py → services/together_ai_service.py} +2 -6
- edsl/inference_services/{XAIService.py → services/xai_service.py} +1 -1
- edsl/inference_services/write_available.py +1 -2
- edsl/instructions/__init__.py +6 -0
- edsl/{surveys/instructions/Instruction.py → instructions/instruction.py} +11 -6
- edsl/{surveys/instructions/InstructionCollection.py → instructions/instruction_collection.py} +10 -5
- edsl/{surveys/InstructionHandler.py → instructions/instruction_handler.py} +3 -3
- edsl/{jobs/interviews → interviews}/ReportErrors.py +2 -2
- edsl/interviews/__init__.py +4 -0
- edsl/{jobs/AnswerQuestionFunctionConstructor.py → interviews/answering_function.py} +45 -18
- edsl/{jobs/interviews/InterviewExceptionEntry.py → interviews/exception_tracking.py} +107 -22
- edsl/interviews/interview.py +638 -0
- edsl/{jobs/interviews/InterviewStatusDictionary.py → interviews/interview_status_dictionary.py} +21 -12
- edsl/{jobs/interviews/InterviewStatusLog.py → interviews/interview_status_log.py} +16 -7
- edsl/{jobs/InterviewTaskManager.py → interviews/interview_task_manager.py} +12 -7
- edsl/{jobs/RequestTokenEstimator.py → interviews/request_token_estimator.py} +8 -3
- edsl/{jobs/interviews/InterviewStatistic.py → interviews/statistics.py} +36 -10
- edsl/invigilators/__init__.py +38 -0
- edsl/invigilators/invigilator_base.py +477 -0
- edsl/{agents/Invigilator.py → invigilators/invigilators.py} +263 -10
- edsl/invigilators/prompt_constructor.py +476 -0
- edsl/{agents → invigilators}/prompt_helpers.py +2 -1
- edsl/{agents/QuestionInstructionPromptBuilder.py → invigilators/question_instructions_prompt_builder.py} +18 -13
- edsl/{agents → invigilators}/question_option_processor.py +96 -21
- edsl/{agents/QuestionTemplateReplacementsBuilder.py → invigilators/question_template_replacements_builder.py} +64 -12
- edsl/jobs/__init__.py +7 -1
- edsl/jobs/async_interview_runner.py +99 -35
- edsl/jobs/check_survey_scenario_compatibility.py +7 -5
- edsl/jobs/data_structures.py +153 -22
- edsl/{exceptions/jobs.py → jobs/exceptions.py} +2 -1
- edsl/jobs/{FetchInvigilator.py → fetch_invigilator.py} +4 -4
- edsl/jobs/{loggers/HTMLTableJobLogger.py → html_table_job_logger.py} +6 -2
- edsl/jobs/{Jobs.py → jobs.py} +313 -167
- edsl/jobs/{JobsChecks.py → jobs_checks.py} +15 -7
- edsl/jobs/{JobsComponentConstructor.py → jobs_component_constructor.py} +19 -17
- edsl/jobs/{InterviewsConstructor.py → jobs_interview_constructor.py} +10 -5
- edsl/jobs/jobs_pricing_estimation.py +347 -0
- edsl/jobs/{JobsRemoteInferenceLogger.py → jobs_remote_inference_logger.py} +4 -3
- edsl/jobs/jobs_runner_asyncio.py +282 -0
- edsl/jobs/{JobsRemoteInferenceHandler.py → remote_inference.py} +19 -22
- edsl/jobs/results_exceptions_handler.py +2 -2
- edsl/key_management/__init__.py +28 -0
- edsl/key_management/key_lookup.py +161 -0
- edsl/{language_models/key_management/KeyLookupBuilder.py → key_management/key_lookup_builder.py} +118 -47
- edsl/key_management/key_lookup_collection.py +82 -0
- edsl/key_management/models.py +218 -0
- edsl/language_models/__init__.py +7 -2
- edsl/language_models/{ComputeCost.py → compute_cost.py} +18 -3
- edsl/{exceptions/language_models.py → language_models/exceptions.py} +2 -1
- edsl/language_models/language_model.py +1080 -0
- edsl/language_models/model.py +10 -25
- edsl/language_models/{ModelList.py → model_list.py} +9 -14
- edsl/language_models/{RawResponseHandler.py → raw_response_handler.py} +1 -1
- edsl/language_models/{RegisterLanguageModelsMeta.py → registry.py} +1 -1
- edsl/language_models/repair.py +4 -4
- edsl/language_models/utilities.py +4 -4
- edsl/notebooks/__init__.py +3 -1
- edsl/notebooks/{Notebook.py → notebook.py} +7 -8
- edsl/prompts/__init__.py +1 -1
- edsl/{exceptions/prompts.py → prompts/exceptions.py} +3 -1
- edsl/prompts/{Prompt.py → prompt.py} +101 -95
- edsl/questions/HTMLQuestion.py +1 -1
- edsl/questions/__init__.py +154 -25
- edsl/questions/answer_validator_mixin.py +1 -1
- edsl/questions/compose_questions.py +4 -3
- edsl/questions/derived/question_likert_five.py +166 -0
- edsl/questions/derived/{QuestionLinearScale.py → question_linear_scale.py} +4 -4
- edsl/questions/derived/{QuestionTopK.py → question_top_k.py} +4 -4
- edsl/questions/derived/{QuestionYesNo.py → question_yes_no.py} +4 -5
- edsl/questions/descriptors.py +24 -30
- edsl/questions/loop_processor.py +65 -19
- edsl/questions/question_base.py +881 -0
- edsl/questions/question_base_gen_mixin.py +15 -16
- edsl/questions/{QuestionBasePromptsMixin.py → question_base_prompts_mixin.py} +2 -2
- edsl/questions/{QuestionBudget.py → question_budget.py} +3 -4
- edsl/questions/{QuestionCheckBox.py → question_check_box.py} +16 -16
- edsl/questions/{QuestionDict.py → question_dict.py} +39 -5
- edsl/questions/{QuestionExtract.py → question_extract.py} +9 -9
- edsl/questions/question_free_text.py +282 -0
- edsl/questions/{QuestionFunctional.py → question_functional.py} +6 -5
- edsl/questions/{QuestionList.py → question_list.py} +6 -7
- edsl/questions/{QuestionMatrix.py → question_matrix.py} +6 -5
- edsl/questions/{QuestionMultipleChoice.py → question_multiple_choice.py} +126 -21
- edsl/questions/{QuestionNumerical.py → question_numerical.py} +5 -5
- edsl/questions/{QuestionRank.py → question_rank.py} +6 -6
- edsl/questions/question_registry.py +4 -9
- edsl/questions/register_questions_meta.py +8 -4
- edsl/questions/response_validator_abc.py +17 -16
- edsl/results/__init__.py +4 -1
- edsl/{exceptions/results.py → results/exceptions.py} +1 -1
- edsl/results/report.py +197 -0
- edsl/results/{Result.py → result.py} +131 -45
- edsl/results/{Results.py → results.py} +365 -220
- edsl/results/results_selector.py +344 -25
- edsl/scenarios/__init__.py +30 -3
- edsl/scenarios/{ConstructDownloadLink.py → construct_download_link.py} +7 -0
- edsl/scenarios/directory_scanner.py +156 -13
- edsl/scenarios/document_chunker.py +186 -0
- edsl/scenarios/exceptions.py +101 -0
- edsl/scenarios/file_methods.py +2 -3
- edsl/scenarios/{FileStore.py → file_store.py} +275 -189
- edsl/scenarios/handlers/__init__.py +14 -14
- edsl/scenarios/handlers/{csv.py → csv_file_store.py} +1 -2
- edsl/scenarios/handlers/{docx.py → docx_file_store.py} +8 -7
- edsl/scenarios/handlers/{html.py → html_file_store.py} +1 -2
- edsl/scenarios/handlers/{jpeg.py → jpeg_file_store.py} +1 -1
- edsl/scenarios/handlers/{json.py → json_file_store.py} +1 -1
- edsl/scenarios/handlers/latex_file_store.py +5 -0
- edsl/scenarios/handlers/{md.py → md_file_store.py} +1 -1
- edsl/scenarios/handlers/{pdf.py → pdf_file_store.py} +2 -2
- edsl/scenarios/handlers/{png.py → png_file_store.py} +1 -1
- edsl/scenarios/handlers/{pptx.py → pptx_file_store.py} +8 -7
- edsl/scenarios/handlers/{py.py → py_file_store.py} +1 -3
- edsl/scenarios/handlers/{sql.py → sql_file_store.py} +2 -1
- edsl/scenarios/handlers/{sqlite.py → sqlite_file_store.py} +2 -3
- edsl/scenarios/handlers/{txt.py → txt_file_store.py} +1 -1
- edsl/scenarios/scenario.py +928 -0
- edsl/scenarios/scenario_join.py +18 -5
- edsl/scenarios/{ScenarioList.py → scenario_list.py} +294 -106
- edsl/scenarios/{ScenarioListPdfMixin.py → scenario_list_pdf_tools.py} +16 -15
- edsl/scenarios/scenario_selector.py +5 -1
- edsl/study/ObjectEntry.py +2 -2
- edsl/study/SnapShot.py +5 -5
- edsl/study/Study.py +18 -19
- edsl/study/__init__.py +6 -4
- edsl/surveys/__init__.py +7 -4
- edsl/surveys/dag/__init__.py +2 -0
- edsl/surveys/{ConstructDAG.py → dag/construct_dag.py} +3 -3
- edsl/surveys/{DAG.py → dag/dag.py} +13 -10
- edsl/surveys/descriptors.py +1 -1
- edsl/surveys/{EditSurvey.py → edit_survey.py} +9 -9
- edsl/{exceptions/surveys.py → surveys/exceptions.py} +1 -2
- edsl/surveys/memory/__init__.py +3 -0
- edsl/surveys/{MemoryPlan.py → memory/memory_plan.py} +10 -9
- edsl/surveys/rules/__init__.py +3 -0
- edsl/surveys/{Rule.py → rules/rule.py} +103 -43
- edsl/surveys/{RuleCollection.py → rules/rule_collection.py} +21 -30
- edsl/surveys/{RuleManager.py → rules/rule_manager.py} +19 -13
- edsl/surveys/survey.py +1743 -0
- edsl/surveys/{SurveyExportMixin.py → survey_export.py} +22 -27
- edsl/surveys/{SurveyFlowVisualization.py → survey_flow_visualization.py} +11 -2
- edsl/surveys/{Simulator.py → survey_simulator.py} +10 -3
- edsl/tasks/__init__.py +32 -0
- edsl/{jobs/tasks/QuestionTaskCreator.py → tasks/question_task_creator.py} +115 -57
- edsl/tasks/task_creators.py +135 -0
- edsl/{jobs/tasks/TaskHistory.py → tasks/task_history.py} +86 -47
- edsl/{jobs/tasks → tasks}/task_status_enum.py +91 -7
- edsl/tasks/task_status_log.py +85 -0
- edsl/tokens/__init__.py +2 -0
- edsl/tokens/interview_token_usage.py +53 -0
- edsl/utilities/PrettyList.py +1 -1
- edsl/utilities/SystemInfo.py +25 -22
- edsl/utilities/__init__.py +29 -21
- edsl/utilities/gcp_bucket/__init__.py +2 -0
- edsl/utilities/gcp_bucket/cloud_storage.py +99 -96
- edsl/utilities/interface.py +44 -536
- edsl/{results/MarkdownToPDF.py → utilities/markdown_to_pdf.py} +13 -5
- edsl/utilities/repair_functions.py +1 -1
- {edsl-0.1.47.dist-info → edsl-0.1.48.dist-info}/METADATA +1 -1
- edsl-0.1.48.dist-info/RECORD +347 -0
- edsl/Base.py +0 -493
- edsl/BaseDiff.py +0 -260
- edsl/agents/InvigilatorBase.py +0 -260
- edsl/agents/PromptConstructor.py +0 -318
- edsl/coop/PriceFetcher.py +0 -54
- edsl/data/Cache.py +0 -582
- edsl/data/CacheEntry.py +0 -238
- edsl/data/SQLiteDict.py +0 -292
- edsl/data/__init__.py +0 -5
- edsl/data/orm.py +0 -10
- edsl/exceptions/cache.py +0 -5
- edsl/exceptions/coop.py +0 -14
- edsl/exceptions/data.py +0 -14
- edsl/exceptions/scenarios.py +0 -29
- edsl/jobs/Answers.py +0 -43
- edsl/jobs/JobsPrompts.py +0 -354
- edsl/jobs/buckets/BucketCollection.py +0 -134
- edsl/jobs/buckets/ModelBuckets.py +0 -65
- edsl/jobs/buckets/TokenBucket.py +0 -283
- edsl/jobs/buckets/TokenBucketClient.py +0 -191
- edsl/jobs/interviews/Interview.py +0 -395
- edsl/jobs/interviews/InterviewExceptionCollection.py +0 -99
- edsl/jobs/interviews/InterviewStatisticsCollection.py +0 -25
- edsl/jobs/runners/JobsRunnerAsyncio.py +0 -163
- edsl/jobs/runners/JobsRunnerStatusData.py +0 -0
- edsl/jobs/tasks/TaskCreators.py +0 -64
- edsl/jobs/tasks/TaskStatusLog.py +0 -23
- edsl/jobs/tokens/InterviewTokenUsage.py +0 -27
- edsl/language_models/LanguageModel.py +0 -635
- edsl/language_models/ServiceDataSources.py +0 -0
- edsl/language_models/key_management/KeyLookup.py +0 -63
- edsl/language_models/key_management/KeyLookupCollection.py +0 -38
- edsl/language_models/key_management/models.py +0 -137
- edsl/questions/QuestionBase.py +0 -544
- edsl/questions/QuestionFreeText.py +0 -130
- edsl/questions/derived/QuestionLikertFive.py +0 -76
- edsl/results/ResultsExportMixin.py +0 -45
- edsl/results/TextEditor.py +0 -50
- edsl/results/results_fetch_mixin.py +0 -33
- edsl/results/results_tools_mixin.py +0 -98
- edsl/scenarios/DocumentChunker.py +0 -104
- edsl/scenarios/Scenario.py +0 -548
- edsl/scenarios/ScenarioHtmlMixin.py +0 -65
- edsl/scenarios/ScenarioListExportMixin.py +0 -45
- edsl/scenarios/handlers/latex.py +0 -5
- edsl/shared.py +0 -1
- edsl/surveys/Survey.py +0 -1301
- edsl/surveys/SurveyQualtricsImport.py +0 -284
- edsl/surveys/SurveyToApp.py +0 -141
- edsl/surveys/instructions/__init__.py +0 -0
- edsl/tools/__init__.py +0 -1
- edsl/tools/clusters.py +0 -192
- edsl/tools/embeddings.py +0 -27
- edsl/tools/embeddings_plotting.py +0 -118
- edsl/tools/plotting.py +0 -112
- edsl/tools/summarize.py +0 -18
- edsl/utilities/data/Registry.py +0 -6
- edsl/utilities/data/__init__.py +0 -1
- edsl/utilities/data/scooter_results.json +0 -1
- edsl-0.1.47.dist-info/RECORD +0 -354
- /edsl/coop/{CoopFunctionsMixin.py → coop_functions.py} +0 -0
- /edsl/{results → dataset/display}/CSSParameterizer.py +0 -0
- /edsl/{language_models/key_management → dataset/display}/__init__.py +0 -0
- /edsl/{results → dataset/display}/table_data_class.py +0 -0
- /edsl/{results → dataset/display}/table_display.css +0 -0
- /edsl/{results/ResultsGGMixin.py → dataset/r/ggplot.py} +0 -0
- /edsl/{results → dataset}/tree_explore.py +0 -0
- /edsl/{surveys/instructions/ChangeInstruction.py → instructions/change_instruction.py} +0 -0
- /edsl/{jobs/interviews → interviews}/interview_status_enum.py +0 -0
- /edsl/jobs/{runners/JobsRunnerStatus.py → jobs_runner_status.py} +0 -0
- /edsl/language_models/{PriceManager.py → price_manager.py} +0 -0
- /edsl/language_models/{fake_openai_call.py → unused/fake_openai_call.py} +0 -0
- /edsl/language_models/{fake_openai_service.py → unused/fake_openai_service.py} +0 -0
- /edsl/notebooks/{NotebookToLaTeX.py → notebook_to_latex.py} +0 -0
- /edsl/{exceptions/questions.py → questions/exceptions.py} +0 -0
- /edsl/questions/{SimpleAskMixin.py → simple_ask_mixin.py} +0 -0
- /edsl/surveys/{Memory.py → memory/memory.py} +0 -0
- /edsl/surveys/{MemoryManagement.py → memory/memory_management.py} +0 -0
- /edsl/surveys/{SurveyCSS.py → survey_css.py} +0 -0
- /edsl/{jobs/tokens/TokenUsage.py → tokens/token_usage.py} +0 -0
- /edsl/{results/MarkdownToDocx.py → utilities/markdown_to_docx.py} +0 -0
- /edsl/{TemplateLoader.py → utilities/template_loader.py} +0 -0
- {edsl-0.1.47.dist-info → edsl-0.1.48.dist-info}/LICENSE +0 -0
- {edsl-0.1.47.dist-info → edsl-0.1.48.dist-info}/WHEEL +0 -0
@@ -0,0 +1,881 @@
|
|
1
|
+
"""
|
2
|
+
Base module for all question types in the EDSL framework.
|
3
|
+
|
4
|
+
The question_base module defines the QuestionBase abstract base class, which serves as
|
5
|
+
the foundation for all question types in EDSL. This module establishes the core
|
6
|
+
functionality, interface, and behavior that all questions must implement.
|
7
|
+
|
8
|
+
Key features of this module include:
|
9
|
+
- Abstract base class that defines the question interface
|
10
|
+
- Core validation and serialization capabilities
|
11
|
+
- Integration with language models and agents
|
12
|
+
- Support for template-based question generation
|
13
|
+
- Connection to response validation and answer processing
|
14
|
+
|
15
|
+
This module is one of the most important in EDSL as it establishes the contract that
|
16
|
+
all question types must follow, enabling consistent behavior across different types
|
17
|
+
of questions while allowing for specialized functionality in derived classes.
|
18
|
+
|
19
|
+
Technical Details:
|
20
|
+
-----------------
|
21
|
+
1. Question Architecture:
|
22
|
+
- QuestionBase is an abstract base class that cannot be instantiated directly
|
23
|
+
- It uses multiple inheritance from several mixins to provide different capabilities
|
24
|
+
- The RegisterQuestionsMeta metaclass enables automatic registration of question types
|
25
|
+
- Each concrete question type must define specific class attributes and methods
|
26
|
+
|
27
|
+
2. Inheritance Hierarchy:
|
28
|
+
- PersistenceMixin: Provides serialization and deserialization via to_dict/from_dict
|
29
|
+
- RepresentationMixin: Provides string representation via __repr__
|
30
|
+
- SimpleAskMixin: Provides the basic asking functionality to interact with models
|
31
|
+
- QuestionBasePromptsMixin: Handles template-based prompt generation
|
32
|
+
- QuestionBaseGenMixin: Connects questions to language models for response generation
|
33
|
+
- AnswerValidatorMixin: Handles validation of answers using response validators
|
34
|
+
|
35
|
+
3. Common Workflow:
|
36
|
+
- User creates a question instance with specific parameters
|
37
|
+
- Question is connected to a language model via the `by()` method
|
38
|
+
- The question generates prompts using templates and scenario variables
|
39
|
+
- The language model generates a response which is parsed and validated
|
40
|
+
- The validated response is returned to the user
|
41
|
+
|
42
|
+
4. Extension Points:
|
43
|
+
- New question types inherit from QuestionBase and define specialized behavior
|
44
|
+
- Custom template files can be defined for specialized prompt generation
|
45
|
+
- Response validators can be customized for different validation requirements
|
46
|
+
- Integration with the survey system using question_name as a key identifier
|
47
|
+
"""
|
48
|
+
|
49
|
+
from __future__ import annotations
|
50
|
+
from abc import ABC
|
51
|
+
from typing import Any, Type, Optional, List, Callable, Union, TypedDict, TYPE_CHECKING
|
52
|
+
|
53
|
+
from .exceptions import QuestionSerializationError
|
54
|
+
|
55
|
+
from ..base import PersistenceMixin, RepresentationMixin, BaseDiff, BaseDiffCollection
|
56
|
+
from ..utilities import remove_edsl_version, is_valid_variable_name
|
57
|
+
|
58
|
+
if TYPE_CHECKING:
|
59
|
+
from ..agents import Agent
|
60
|
+
from ..scenarios import Scenario
|
61
|
+
from ..surveys import Survey
|
62
|
+
|
63
|
+
from .descriptors import QuestionNameDescriptor, QuestionTextDescriptor
|
64
|
+
from .answer_validator_mixin import AnswerValidatorMixin
|
65
|
+
from .register_questions_meta import RegisterQuestionsMeta
|
66
|
+
from .simple_ask_mixin import SimpleAskMixin
|
67
|
+
from .question_base_prompts_mixin import QuestionBasePromptsMixin
|
68
|
+
from .question_base_gen_mixin import QuestionBaseGenMixin
|
69
|
+
|
70
|
+
if TYPE_CHECKING:
|
71
|
+
from .response_validator_abc import ResponseValidatorABC
|
72
|
+
from ..language_models import LanguageModel
|
73
|
+
from ..results import Results
|
74
|
+
from ..jobs import Jobs
|
75
|
+
|
76
|
+
|
77
|
+
class QuestionBase(
|
78
|
+
PersistenceMixin,
|
79
|
+
RepresentationMixin,
|
80
|
+
SimpleAskMixin,
|
81
|
+
QuestionBasePromptsMixin,
|
82
|
+
QuestionBaseGenMixin,
|
83
|
+
ABC,
|
84
|
+
AnswerValidatorMixin,
|
85
|
+
metaclass=RegisterQuestionsMeta,
|
86
|
+
):
|
87
|
+
"""
|
88
|
+
Abstract base class for all question types in EDSL.
|
89
|
+
|
90
|
+
QuestionBase defines the core interface and behavior that all question types must
|
91
|
+
implement. It provides the foundation for asking questions to agents, validating
|
92
|
+
responses, generating prompts, and integrating with the rest of the EDSL framework.
|
93
|
+
|
94
|
+
The class inherits from multiple mixins to provide different capabilities:
|
95
|
+
- PersistenceMixin: Serialization and deserialization
|
96
|
+
- RepresentationMixin: String representation
|
97
|
+
- SimpleAskMixin: Basic asking functionality
|
98
|
+
- QuestionBasePromptsMixin: Template-based prompt generation
|
99
|
+
- QuestionBaseGenMixin: Generate responses with language models
|
100
|
+
- AnswerValidatorMixin: Response validation
|
101
|
+
|
102
|
+
It also uses the RegisterQuestionsMeta metaclass to enforce constraints on child classes
|
103
|
+
and automatically register them for serialization and runtime use.
|
104
|
+
|
105
|
+
Class attributes:
|
106
|
+
question_name (str): Name of the question, used as an identifier
|
107
|
+
question_text (str): The actual text of the question to be asked
|
108
|
+
|
109
|
+
Required attributes in derived classes:
|
110
|
+
question_type (str): String identifier for the question type
|
111
|
+
_response_model (Type): Pydantic model class for validating responses
|
112
|
+
response_validator_class (Type): Validator class for responses
|
113
|
+
|
114
|
+
Key Methods:
|
115
|
+
by(model): Connect this question to a language model for answering
|
116
|
+
run(): Execute the question with the connected language model
|
117
|
+
duplicate(): Create an exact copy of this question
|
118
|
+
is_valid_question_name(): Verify the question_name is valid
|
119
|
+
|
120
|
+
Lifecycle:
|
121
|
+
1. Instantiation: A question is created with specific parameters
|
122
|
+
2. Connection: The question is connected to a language model via by()
|
123
|
+
3. Execution: The question is run to generate a response
|
124
|
+
4. Validation: The response is validated based on the question type
|
125
|
+
5. Result: The validated response is returned for analysis
|
126
|
+
|
127
|
+
Template System:
|
128
|
+
Questions use Jinja2 templates for generating prompts. Each question type
|
129
|
+
has associated template files:
|
130
|
+
- answering_instructions.jinja: Instructions for how the model should answer
|
131
|
+
- question_presentation.jinja: Format for how the question is presented
|
132
|
+
Templates support variable substitution using scenario variables.
|
133
|
+
|
134
|
+
Response Validation:
|
135
|
+
Each question type has a dedicated response validator that:
|
136
|
+
- Enforces the expected response structure
|
137
|
+
- Ensures the response is valid for the question type
|
138
|
+
- Attempts to fix invalid responses when possible
|
139
|
+
- Uses Pydantic models for schema validation
|
140
|
+
|
141
|
+
Example:
|
142
|
+
Derived classes must define the required attributes:
|
143
|
+
|
144
|
+
```python
|
145
|
+
class FreeTextQuestion(QuestionBase):
|
146
|
+
question_type = "free_text"
|
147
|
+
_response_model = FreeTextResponse
|
148
|
+
response_validator_class = FreeTextResponseValidator
|
149
|
+
|
150
|
+
def __init__(self, question_name, question_text, **kwargs):
|
151
|
+
self.question_name = question_name
|
152
|
+
self.question_text = question_text
|
153
|
+
# Additional initialization as needed
|
154
|
+
```
|
155
|
+
|
156
|
+
Using a question:
|
157
|
+
|
158
|
+
```python
|
159
|
+
# Create a question
|
160
|
+
question = FreeTextQuestion(
|
161
|
+
question_name="opinion",
|
162
|
+
question_text="What do you think about AI?"
|
163
|
+
)
|
164
|
+
|
165
|
+
# Connect to a language model and run
|
166
|
+
from edsl.language_models import Model
|
167
|
+
model = Model()
|
168
|
+
result = question.by(model).run()
|
169
|
+
|
170
|
+
# Access the answer
|
171
|
+
answer = result.select("answer.opinion").to_list()[0]
|
172
|
+
print(f"The model's opinion: {answer}")
|
173
|
+
```
|
174
|
+
|
175
|
+
Notes:
|
176
|
+
- QuestionBase is abstract and cannot be instantiated directly
|
177
|
+
- Child classes must implement required methods and attributes
|
178
|
+
- The RegisterQuestionsMeta metaclass handles registration of question types
|
179
|
+
- Questions can be serialized to and from dictionaries for storage
|
180
|
+
- Questions can be used independently or as part of surveys
|
181
|
+
"""
|
182
|
+
|
183
|
+
question_name: str = QuestionNameDescriptor()
|
184
|
+
question_text: str = QuestionTextDescriptor()
|
185
|
+
|
186
|
+
_answering_instructions = None
|
187
|
+
_question_presentation = None
|
188
|
+
|
189
|
+
def is_valid_question_name(self) -> bool:
|
190
|
+
"""
|
191
|
+
Check if the question name is a valid Python identifier.
|
192
|
+
|
193
|
+
This method validates that the question_name attribute is a valid Python
|
194
|
+
variable name according to Python's syntax rules. This is important because
|
195
|
+
question names are often used as identifiers in various parts of the system.
|
196
|
+
|
197
|
+
Returns:
|
198
|
+
bool: True if the question name is a valid Python identifier, False otherwise.
|
199
|
+
|
200
|
+
Examples:
|
201
|
+
>>> from edsl.questions import QuestionFreeText
|
202
|
+
>>> q = QuestionFreeText(question_name="valid_name", question_text="Text")
|
203
|
+
>>> q.is_valid_question_name()
|
204
|
+
True
|
205
|
+
|
206
|
+
>>> q = QuestionFreeText(question_name="123invalid", question_text="Text")
|
207
|
+
Traceback (most recent call last):
|
208
|
+
...
|
209
|
+
edsl.questions.exceptions.QuestionCreationValidationError: `question_name` is not a valid variable name (got 123invalid).
|
210
|
+
"""
|
211
|
+
return is_valid_variable_name(self.question_name)
|
212
|
+
|
213
|
+
@property
|
214
|
+
def response_validator(self) -> "ResponseValidatorABC":
|
215
|
+
"""
|
216
|
+
Get the appropriate validator for this question type.
|
217
|
+
|
218
|
+
This property lazily creates and returns a response validator instance specific
|
219
|
+
to this question type. The validator is created using the ResponseValidatorFactory,
|
220
|
+
which selects the appropriate validator class based on the question's type.
|
221
|
+
|
222
|
+
Returns:
|
223
|
+
ResponseValidatorABC: An instance of the appropriate validator for this question.
|
224
|
+
|
225
|
+
Notes:
|
226
|
+
- Each question type has its own validator class defined in the class attribute
|
227
|
+
response_validator_class
|
228
|
+
- The validator is responsible for ensuring responses conform to the expected
|
229
|
+
format and constraints for this question type
|
230
|
+
"""
|
231
|
+
from .response_validator_factory import ResponseValidatorFactory
|
232
|
+
|
233
|
+
rvf = ResponseValidatorFactory(self)
|
234
|
+
return rvf.response_validator
|
235
|
+
|
236
|
+
def duplicate(self) -> "QuestionBase":
|
237
|
+
"""
|
238
|
+
Create an exact copy of this question instance.
|
239
|
+
|
240
|
+
This method creates a new instance of the question with identical attributes
|
241
|
+
by serializing the current instance to a dictionary and then deserializing
|
242
|
+
it back into a new instance.
|
243
|
+
|
244
|
+
Returns:
|
245
|
+
QuestionBase: A new instance of the same question type with identical attributes.
|
246
|
+
|
247
|
+
Examples:
|
248
|
+
>>> from edsl.questions import QuestionFreeText
|
249
|
+
>>> original = QuestionFreeText(question_name="q1", question_text="Hello?")
|
250
|
+
>>> copy = original.duplicate()
|
251
|
+
>>> original.question_name == copy.question_name
|
252
|
+
True
|
253
|
+
>>> original is copy
|
254
|
+
False
|
255
|
+
"""
|
256
|
+
return self.from_dict(self.to_dict())
|
257
|
+
|
258
|
+
@property
|
259
|
+
def fake_data_factory(self):
|
260
|
+
"""
|
261
|
+
Create and return a factory for generating fake response data.
|
262
|
+
|
263
|
+
This property lazily creates a factory class based on Pydantic's ModelFactory
|
264
|
+
that can generate fake data conforming to the question's response model.
|
265
|
+
The factory is cached after first creation for efficiency.
|
266
|
+
|
267
|
+
Returns:
|
268
|
+
ModelFactory: A factory class that can generate fake data for this question type.
|
269
|
+
|
270
|
+
Notes:
|
271
|
+
- Uses polyfactory to generate valid fake data instances
|
272
|
+
- The response model for the question defines the structure of the generated data
|
273
|
+
- Primarily used for testing and simulation purposes
|
274
|
+
"""
|
275
|
+
if not hasattr(self, "_fake_data_factory"):
|
276
|
+
from polyfactory.factories.pydantic_factory import ModelFactory
|
277
|
+
|
278
|
+
class FakeData(ModelFactory[self.response_model]): ...
|
279
|
+
|
280
|
+
self._fake_data_factory = FakeData
|
281
|
+
return self._fake_data_factory
|
282
|
+
|
283
|
+
def _simulate_answer(self, human_readable: bool = False) -> dict:
|
284
|
+
"""
|
285
|
+
Generate a simulated valid answer for this question.
|
286
|
+
|
287
|
+
This method creates a plausible answer that would pass validation for this
|
288
|
+
question type. It's primarily used for testing, examples, and debugging purposes.
|
289
|
+
|
290
|
+
Args:
|
291
|
+
human_readable: If True, converts code-based answers to their human-readable
|
292
|
+
text equivalents for multiple choice and similar questions.
|
293
|
+
|
294
|
+
Returns:
|
295
|
+
dict: A dictionary containing a simulated valid answer with appropriate
|
296
|
+
structure for this question type.
|
297
|
+
|
298
|
+
Examples:
|
299
|
+
>>> from edsl import QuestionFreeText as Q
|
300
|
+
>>> answer = Q.example()._simulate_answer()
|
301
|
+
>>> "answer" in answer and "generated_tokens" in answer
|
302
|
+
True
|
303
|
+
|
304
|
+
Notes:
|
305
|
+
- Free text questions have special handling with a predefined response
|
306
|
+
- Other question types use the fake_data_factory to generate valid responses
|
307
|
+
- For questions with options, the human_readable parameter determines whether
|
308
|
+
indices or actual text options are returned
|
309
|
+
"""
|
310
|
+
if self.question_type == "free_text":
|
311
|
+
return {"answer": "Hello, how are you?", 'generated_tokens': "Hello, how are you?"}
|
312
|
+
|
313
|
+
simulated_answer = self.fake_data_factory.build().dict()
|
314
|
+
if human_readable and hasattr(self, "question_options") and self.use_code:
|
315
|
+
simulated_answer["answer"] = [
|
316
|
+
self.question_options[index] for index in simulated_answer["answer"]
|
317
|
+
]
|
318
|
+
return simulated_answer
|
319
|
+
|
320
|
+
class ValidatedAnswer(TypedDict):
|
321
|
+
"""
|
322
|
+
Type definition for a validated answer to a question.
|
323
|
+
|
324
|
+
This TypedDict defines the structure of a validated answer, which includes
|
325
|
+
the actual answer value, an optional comment, and optional generated tokens
|
326
|
+
information for tracking LLM token usage.
|
327
|
+
|
328
|
+
Attributes:
|
329
|
+
answer: The validated answer value, type depends on question type
|
330
|
+
comment: Optional string comment or explanation for the answer
|
331
|
+
generated_tokens: Optional string containing raw LLM output for token tracking
|
332
|
+
"""
|
333
|
+
answer: Any
|
334
|
+
comment: Optional[str]
|
335
|
+
generated_tokens: Optional[str]
|
336
|
+
|
337
|
+
def _validate_answer(
|
338
|
+
self, answer: dict, replacement_dict: dict = None
|
339
|
+
) -> ValidatedAnswer:
|
340
|
+
"""
|
341
|
+
Validate a raw answer against this question's constraints.
|
342
|
+
|
343
|
+
This method applies the appropriate validator for this question type to the
|
344
|
+
provided answer dictionary, ensuring it conforms to the expected structure
|
345
|
+
and constraints.
|
346
|
+
|
347
|
+
Args:
|
348
|
+
answer: Dictionary containing the raw answer to validate.
|
349
|
+
replacement_dict: Optional dictionary of replacements to apply during
|
350
|
+
validation for template variables.
|
351
|
+
|
352
|
+
Returns:
|
353
|
+
ValidatedAnswer: A dictionary containing the validated answer with the
|
354
|
+
structure defined by ValidatedAnswer TypedDict.
|
355
|
+
|
356
|
+
Raises:
|
357
|
+
QuestionAnswerValidationError: If the answer fails validation.
|
358
|
+
|
359
|
+
Examples:
|
360
|
+
>>> from edsl.questions import QuestionFreeText as Q
|
361
|
+
>>> Q.example()._validate_answer({'answer': 'Hello', 'generated_tokens': 'Hello'})
|
362
|
+
{'answer': 'Hello', 'generated_tokens': 'Hello'}
|
363
|
+
"""
|
364
|
+
return self.response_validator.validate(answer, replacement_dict)
|
365
|
+
|
366
|
+
@property
|
367
|
+
def name(self) -> str:
|
368
|
+
"""
|
369
|
+
Get the question name.
|
370
|
+
|
371
|
+
This property is a simple alias for question_name that provides a consistent
|
372
|
+
interface shared with other EDSL components like Instructions.
|
373
|
+
|
374
|
+
Returns:
|
375
|
+
str: The question name.
|
376
|
+
"""
|
377
|
+
return self.question_name
|
378
|
+
|
379
|
+
def __hash__(self) -> int:
|
380
|
+
"""
|
381
|
+
Calculate a hash value for this question instance.
|
382
|
+
|
383
|
+
This method returns a deterministic hash based on the serialized dictionary
|
384
|
+
representation of the question. This allows questions to be used in sets and
|
385
|
+
as dictionary keys.
|
386
|
+
|
387
|
+
Returns:
|
388
|
+
int: A hash value for this question.
|
389
|
+
|
390
|
+
Examples:
|
391
|
+
>>> from edsl import QuestionFreeText as Q
|
392
|
+
>>> q1 = Q.example()
|
393
|
+
>>> q2 = q1.duplicate()
|
394
|
+
>>> hash(q1) == hash(q2)
|
395
|
+
True
|
396
|
+
"""
|
397
|
+
from ..utilities import dict_hash
|
398
|
+
|
399
|
+
return dict_hash(self.to_dict(add_edsl_version=False))
|
400
|
+
|
401
|
+
@property
|
402
|
+
def data(self) -> dict:
|
403
|
+
"""Return a dictionary of question attributes **except** for question_type.
|
404
|
+
|
405
|
+
>>> from edsl.questions import QuestionFreeText as Q
|
406
|
+
>>> Q.example().data
|
407
|
+
{'question_name': 'how_are_you', 'question_text': 'How are you?'}
|
408
|
+
"""
|
409
|
+
exclude_list = [
|
410
|
+
"question_type",
|
411
|
+
# "_include_comment",
|
412
|
+
"_fake_data_factory",
|
413
|
+
# "_use_code",
|
414
|
+
"_model_instructions",
|
415
|
+
]
|
416
|
+
only_if_not_na_list = ["_answering_instructions", "_question_presentation"]
|
417
|
+
|
418
|
+
only_if_not_default_list = {"_include_comment": True, "_use_code": False}
|
419
|
+
|
420
|
+
def ok(key, value):
|
421
|
+
if not key.startswith("_"):
|
422
|
+
return False
|
423
|
+
if key in exclude_list:
|
424
|
+
return False
|
425
|
+
if key in only_if_not_na_list and value is None:
|
426
|
+
return False
|
427
|
+
if (
|
428
|
+
key in only_if_not_default_list
|
429
|
+
and value == only_if_not_default_list[key]
|
430
|
+
):
|
431
|
+
return False
|
432
|
+
|
433
|
+
return True
|
434
|
+
|
435
|
+
candidate_data = {
|
436
|
+
k.replace("_", "", 1): v for k, v in self.__dict__.items() if ok(k, v)
|
437
|
+
}
|
438
|
+
|
439
|
+
if "func" in candidate_data:
|
440
|
+
func = candidate_data.pop("func")
|
441
|
+
import inspect
|
442
|
+
|
443
|
+
candidate_data["function_source_code"] = inspect.getsource(func)
|
444
|
+
|
445
|
+
return candidate_data
|
446
|
+
|
447
|
+
def to_dict(self, add_edsl_version: bool = True):
|
448
|
+
"""Convert the question to a dictionary that includes the question type (used in deserialization).
|
449
|
+
|
450
|
+
>>> from edsl.questions import QuestionFreeText as Q; Q.example().to_dict(add_edsl_version = False)
|
451
|
+
{'question_name': 'how_are_you', 'question_text': 'How are you?', 'question_type': 'free_text'}
|
452
|
+
"""
|
453
|
+
candidate_data = self.data.copy()
|
454
|
+
candidate_data["question_type"] = self.question_type
|
455
|
+
d = {key: value for key, value in candidate_data.items() if value is not None}
|
456
|
+
if add_edsl_version:
|
457
|
+
from .. import __version__
|
458
|
+
|
459
|
+
d["edsl_version"] = __version__
|
460
|
+
d["edsl_class_name"] = "QuestionBase"
|
461
|
+
|
462
|
+
return d
|
463
|
+
|
464
|
+
@classmethod
|
465
|
+
@remove_edsl_version
|
466
|
+
def from_dict(cls, data: dict) -> "QuestionBase":
|
467
|
+
"""
|
468
|
+
Create a question instance from a dictionary representation.
|
469
|
+
|
470
|
+
This class method deserializes a question from a dictionary representation,
|
471
|
+
typically created by the to_dict method. It looks up the appropriate question
|
472
|
+
class based on the question_type field and constructs an instance of that class.
|
473
|
+
|
474
|
+
Args:
|
475
|
+
data: Dictionary representation of a question, must contain a 'question_type' field.
|
476
|
+
|
477
|
+
Returns:
|
478
|
+
QuestionBase: An instance of the appropriate question subclass.
|
479
|
+
|
480
|
+
Raises:
|
481
|
+
QuestionSerializationError: If the data is missing the question_type field or
|
482
|
+
if no question class is registered for the given type.
|
483
|
+
|
484
|
+
Examples:
|
485
|
+
>>> from edsl.questions import QuestionFreeText
|
486
|
+
>>> original = QuestionFreeText.example()
|
487
|
+
>>> serialized = original.to_dict()
|
488
|
+
>>> deserialized = QuestionBase.from_dict(serialized)
|
489
|
+
>>> original.question_text == deserialized.question_text
|
490
|
+
True
|
491
|
+
>>> isinstance(deserialized, QuestionFreeText)
|
492
|
+
True
|
493
|
+
|
494
|
+
Notes:
|
495
|
+
- The @remove_edsl_version decorator removes EDSL version information from the
|
496
|
+
dictionary before processing
|
497
|
+
- Special handling is implemented for certain question types like linear_scale
|
498
|
+
- Model instructions, if present, are handled separately to ensure proper initialization
|
499
|
+
"""
|
500
|
+
local_data = data.copy()
|
501
|
+
|
502
|
+
try:
|
503
|
+
question_type = local_data.pop("question_type")
|
504
|
+
if question_type == "linear_scale":
|
505
|
+
# This is a fix for issue https://github.com/expectedparrot/edsl/issues/165
|
506
|
+
options_labels = local_data.get("option_labels", None)
|
507
|
+
if options_labels:
|
508
|
+
options_labels = {
|
509
|
+
int(key): value for key, value in options_labels.items()
|
510
|
+
}
|
511
|
+
local_data["option_labels"] = options_labels
|
512
|
+
except:
|
513
|
+
raise QuestionSerializationError(
|
514
|
+
f"Data does not have a 'question_type' field (got {data})."
|
515
|
+
)
|
516
|
+
from .question_registry import get_question_class
|
517
|
+
|
518
|
+
try:
|
519
|
+
question_class = get_question_class(question_type)
|
520
|
+
except ValueError:
|
521
|
+
raise QuestionSerializationError(
|
522
|
+
f"No question registered with question_type {question_type}"
|
523
|
+
)
|
524
|
+
|
525
|
+
if "model_instructions" in local_data:
|
526
|
+
model_instructions = local_data.pop("model_instructions")
|
527
|
+
new_q = question_class(**local_data)
|
528
|
+
new_q.model_instructions = model_instructions
|
529
|
+
return new_q
|
530
|
+
|
531
|
+
return question_class(**local_data)
|
532
|
+
|
533
|
+
@classmethod
|
534
|
+
def _get_test_model(cls, canned_response: Optional[str] = None) -> "LanguageModel":
|
535
|
+
"""
|
536
|
+
Create a test language model with optional predefined response.
|
537
|
+
|
538
|
+
This helper method creates a test language model that can be used for testing
|
539
|
+
questions without making actual API calls to language model providers.
|
540
|
+
|
541
|
+
Args:
|
542
|
+
canned_response: Optional predefined response the model will return for any prompt.
|
543
|
+
|
544
|
+
Returns:
|
545
|
+
LanguageModel: A test language model instance.
|
546
|
+
|
547
|
+
Notes:
|
548
|
+
- The test model does not make external API calls
|
549
|
+
- When canned_response is provided, the model will always return that response
|
550
|
+
- Used primarily for testing, demonstrations, and examples
|
551
|
+
"""
|
552
|
+
from ..language_models import LanguageModel
|
553
|
+
|
554
|
+
return LanguageModel.example(canned_response=canned_response, test_model=True)
|
555
|
+
|
556
|
+
@classmethod
|
557
|
+
def run_example(
|
558
|
+
cls,
|
559
|
+
show_answer: bool = True,
|
560
|
+
model: Optional["LanguageModel"] = None,
|
561
|
+
cache: bool = False,
|
562
|
+
disable_remote_cache: bool = False,
|
563
|
+
disable_remote_inference: bool = False,
|
564
|
+
**kwargs,
|
565
|
+
) -> "Results":
|
566
|
+
"""
|
567
|
+
Run the example question with a language model and return results.
|
568
|
+
|
569
|
+
This class method creates an example instance of the question, asks it using
|
570
|
+
the provided language model, and returns the results. It's primarily used for
|
571
|
+
demonstrations, documentation, and testing.
|
572
|
+
|
573
|
+
Args:
|
574
|
+
show_answer: If True, returns only the answer portion of the results.
|
575
|
+
If False, returns the full results.
|
576
|
+
model: Language model to use for answering. If None, creates a default model.
|
577
|
+
cache: Whether to use local caching for the model call.
|
578
|
+
disable_remote_cache: Whether to disable remote caching.
|
579
|
+
disable_remote_inference: Whether to disable remote inference.
|
580
|
+
**kwargs: Additional keyword arguments to pass to the example method.
|
581
|
+
|
582
|
+
Returns:
|
583
|
+
Results: Either the full results or just the answer portion, depending on show_answer.
|
584
|
+
|
585
|
+
Examples:
|
586
|
+
>>> from edsl.language_models import LanguageModel
|
587
|
+
>>> from edsl import QuestionFreeText as Q
|
588
|
+
>>> m = Q._get_test_model(canned_response="Yo, what's up?")
|
589
|
+
>>> results = Q.run_example(show_answer=True, model=m,
|
590
|
+
... disable_remote_cache=True, disable_remote_inference=True)
|
591
|
+
>>> "answer" in str(results)
|
592
|
+
True
|
593
|
+
|
594
|
+
Notes:
|
595
|
+
- This method is useful for quick demonstrations of question behavior
|
596
|
+
- The disable_remote_* parameters are useful for offline testing
|
597
|
+
- Additional parameters to customize the example can be passed via kwargs
|
598
|
+
"""
|
599
|
+
if model is None:
|
600
|
+
from ..language_models import Model
|
601
|
+
|
602
|
+
model = Model()
|
603
|
+
results = (
|
604
|
+
cls.example(**kwargs)
|
605
|
+
.by(model)
|
606
|
+
.run(
|
607
|
+
cache=cache,
|
608
|
+
disable_remote_cache=disable_remote_cache,
|
609
|
+
disable_remote_inference=disable_remote_inference,
|
610
|
+
)
|
611
|
+
)
|
612
|
+
if show_answer:
|
613
|
+
return results.select("answer.*")
|
614
|
+
else:
|
615
|
+
return results
|
616
|
+
|
617
|
+
def __call__(
|
618
|
+
self,
|
619
|
+
just_answer: bool = True,
|
620
|
+
model: Optional["LanguageModel"] = None,
|
621
|
+
agent: Optional["Agent"] = None,
|
622
|
+
disable_remote_cache: bool = False,
|
623
|
+
disable_remote_inference: bool = False,
|
624
|
+
verbose: bool = False,
|
625
|
+
**kwargs,
|
626
|
+
) -> Union[Any, "Results"]:
|
627
|
+
"""Call the question.
|
628
|
+
|
629
|
+
|
630
|
+
>>> from edsl import QuestionFreeText as Q
|
631
|
+
>>> from edsl import Model
|
632
|
+
>>> m = Model("test", canned_response = "Yo, what's up?")
|
633
|
+
>>> q = Q(question_name = "color", question_text = "What is your favorite color?")
|
634
|
+
>>> q(model = m, disable_remote_cache = True, disable_remote_inference = True, cache = False)
|
635
|
+
"Yo, what's up?"
|
636
|
+
|
637
|
+
"""
|
638
|
+
survey = self.to_survey()
|
639
|
+
results = survey(
|
640
|
+
model=model,
|
641
|
+
agent=agent,
|
642
|
+
**kwargs,
|
643
|
+
verbose=verbose,
|
644
|
+
disable_remote_cache=disable_remote_cache,
|
645
|
+
disable_remote_inference=disable_remote_inference,
|
646
|
+
)
|
647
|
+
if just_answer:
|
648
|
+
return results.select(f"answer.{self.question_name}").first()
|
649
|
+
else:
|
650
|
+
return results
|
651
|
+
|
652
|
+
def run(self, *args, **kwargs) -> "Results":
|
653
|
+
"""Turn a single question into a survey and runs it."""
|
654
|
+
return self.to_survey().run(*args, **kwargs)
|
655
|
+
|
656
|
+
def using(self, *args, **kwargs) -> "Jobs":
|
657
|
+
"""Turn a single question into a survey and then a Job."""
|
658
|
+
return self.to_survey().to_jobs().using(*args, **kwargs)
|
659
|
+
|
660
|
+
async def run_async(
|
661
|
+
self,
|
662
|
+
just_answer: bool = True,
|
663
|
+
model: Optional["LanguageModel"] = None,
|
664
|
+
agent: Optional["Agent"] = None,
|
665
|
+
disable_remote_inference: bool = False,
|
666
|
+
**kwargs,
|
667
|
+
) -> Union[Any, "Results"]:
|
668
|
+
"""Call the question asynchronously.
|
669
|
+
|
670
|
+
>>> import asyncio
|
671
|
+
>>> from edsl.questions import QuestionFreeText as Q
|
672
|
+
>>> m = Q._get_test_model(canned_response = "Blue")
|
673
|
+
>>> q = Q(question_name = "color", question_text = "What is your favorite color?")
|
674
|
+
>>> async def test_run_async(): result = await q.run_async(model=m, disable_remote_inference = True, disable_remote_cache = True); print(result)
|
675
|
+
>>> asyncio.run(test_run_async())
|
676
|
+
Blue
|
677
|
+
"""
|
678
|
+
survey = self.to_survey()
|
679
|
+
results = await survey.run_async(
|
680
|
+
model=model,
|
681
|
+
agent=agent,
|
682
|
+
disable_remote_inference=disable_remote_inference,
|
683
|
+
**kwargs,
|
684
|
+
)
|
685
|
+
if just_answer:
|
686
|
+
return results.select(f"answer.{self.question_name}").first()
|
687
|
+
else:
|
688
|
+
return results
|
689
|
+
|
690
|
+
def __getitem__(self, key: str) -> Any:
|
691
|
+
"""Get an attribute of the question so it can be treated like a dictionary.
|
692
|
+
|
693
|
+
>>> from edsl.questions import QuestionFreeText as Q
|
694
|
+
>>> Q.example()['question_text']
|
695
|
+
'How are you?'
|
696
|
+
"""
|
697
|
+
try:
|
698
|
+
return getattr(self, key)
|
699
|
+
except TypeError:
|
700
|
+
raise KeyError(f"Question has no attribute {key} of type {type(key)}")
|
701
|
+
|
702
|
+
def __repr__(self) -> str:
|
703
|
+
"""Return a string representation of the question. Should be able to be used to reconstruct the question.
|
704
|
+
|
705
|
+
>>> from edsl import QuestionFreeText as Q
|
706
|
+
>>> repr(Q.example())
|
707
|
+
'Question(\\'free_text\\', question_name = \"""how_are_you\""", question_text = \"""How are you?\""")'
|
708
|
+
"""
|
709
|
+
items = [
|
710
|
+
f'{k} = """{v}"""' if isinstance(v, str) else f"{k} = {v}"
|
711
|
+
for k, v in self.data.items()
|
712
|
+
if k != "question_type"
|
713
|
+
]
|
714
|
+
question_type = self.to_dict().get("question_type", "None")
|
715
|
+
return f"Question('{question_type}', {', '.join(items)})"
|
716
|
+
|
717
|
+
def __eq__(self, other: Union[Any, Type[QuestionBase]]) -> bool:
|
718
|
+
"""Check if two questions are equal. Equality is defined as having the .to_dict().
|
719
|
+
|
720
|
+
>>> from edsl import QuestionFreeText as Q
|
721
|
+
>>> q1 = Q.example()
|
722
|
+
>>> q2 = Q.example()
|
723
|
+
>>> q1 == q2
|
724
|
+
True
|
725
|
+
>>> q1.question_text = "How are you John?"
|
726
|
+
>>> q1 == q2
|
727
|
+
False
|
728
|
+
|
729
|
+
"""
|
730
|
+
return hash(self) == hash(other)
|
731
|
+
|
732
|
+
def __sub__(self, other) -> BaseDiff:
|
733
|
+
"""Return the difference between two objects.
|
734
|
+
>>> from edsl import QuestionFreeText as Q
|
735
|
+
>>> q1 = Q.example()
|
736
|
+
>>> q2 = q1.copy()
|
737
|
+
>>> q2.question_text = "How are you John?"
|
738
|
+
>>> diff = q1 - q2
|
739
|
+
"""
|
740
|
+
|
741
|
+
return BaseDiff(other, self)
|
742
|
+
|
743
|
+
# TODO: Throws an error that should be addressed at QuestionFunctional
|
744
|
+
def __add__(self, other_question_or_diff):
|
745
|
+
"""
|
746
|
+
Compose two questions into a single question.
|
747
|
+
"""
|
748
|
+
if isinstance(other_question_or_diff, BaseDiff) or isinstance(
|
749
|
+
other_question_or_diff, BaseDiffCollection
|
750
|
+
):
|
751
|
+
return other_question_or_diff.apply(self)
|
752
|
+
|
753
|
+
def _translate_answer_code_to_answer(
|
754
|
+
self, answer, scenario: Optional["Scenario"] = None
|
755
|
+
):
|
756
|
+
"""There is over-ridden by child classes that ask for codes."""
|
757
|
+
return answer
|
758
|
+
|
759
|
+
def add_question(self, other: QuestionBase) -> "Survey":
|
760
|
+
"""Add a question to this question by turning them into a survey with two questions.
|
761
|
+
|
762
|
+
>>> from edsl.questions import QuestionFreeText as Q
|
763
|
+
>>> from edsl.questions import QuestionMultipleChoice as QMC
|
764
|
+
>>> s = Q.example().add_question(QMC.example())
|
765
|
+
>>> len(s.questions)
|
766
|
+
2
|
767
|
+
"""
|
768
|
+
return self.to_survey().add_question(other)
|
769
|
+
|
770
|
+
def to_survey(self) -> "Survey":
|
771
|
+
"""Turn a single question into a survey.
|
772
|
+
>>> from edsl import QuestionFreeText as Q
|
773
|
+
>>> Q.example().to_survey().questions[0].question_name
|
774
|
+
'how_are_you'
|
775
|
+
"""
|
776
|
+
from ..surveys import Survey
|
777
|
+
|
778
|
+
return Survey([self])
|
779
|
+
|
780
|
+
def humanize(
|
781
|
+
self,
|
782
|
+
project_name: str = "Project",
|
783
|
+
survey_description: Optional[str] = None,
|
784
|
+
survey_alias: Optional[str] = None,
|
785
|
+
survey_visibility: Optional["VisibilityType"] = "unlisted",
|
786
|
+
) -> dict:
|
787
|
+
"""
|
788
|
+
Turn a single question into a survey and send the survey to Coop.
|
789
|
+
|
790
|
+
Then, create a project on Coop so you can share the survey with human respondents.
|
791
|
+
"""
|
792
|
+
s = self.to_survey()
|
793
|
+
project_details = s.humanize(
|
794
|
+
project_name, survey_description, survey_alias, survey_visibility
|
795
|
+
)
|
796
|
+
return project_details
|
797
|
+
|
798
|
+
def by(self, *args) -> "Jobs":
|
799
|
+
"""Turn a single question into a survey and then a Job."""
|
800
|
+
from ..surveys import Survey
|
801
|
+
|
802
|
+
s = Survey([self])
|
803
|
+
return s.by(*args)
|
804
|
+
|
805
|
+
def human_readable(self) -> str:
|
806
|
+
"""Print the question in a human readable format.
|
807
|
+
|
808
|
+
>>> from edsl.questions import QuestionFreeText
|
809
|
+
>>> QuestionFreeText.example().human_readable()
|
810
|
+
'Question Type: free_text\\nQuestion: How are you?'
|
811
|
+
"""
|
812
|
+
lines = []
|
813
|
+
lines.append(f"Question Type: {self.question_type}")
|
814
|
+
lines.append(f"Question: {self.question_text}")
|
815
|
+
if hasattr(self, "question_options"):
|
816
|
+
lines.append("Please name the option you choose from the following.:")
|
817
|
+
for index, option in enumerate(self.question_options):
|
818
|
+
lines.append(f"{option}")
|
819
|
+
return "\n".join(lines)
|
820
|
+
|
821
|
+
def html(
|
822
|
+
self,
|
823
|
+
scenario: Optional[dict] = None,
|
824
|
+
agent: Optional[dict] = {},
|
825
|
+
answers: Optional[dict] = None,
|
826
|
+
include_question_name: bool = False,
|
827
|
+
height: Optional[int] = None,
|
828
|
+
width: Optional[int] = None,
|
829
|
+
iframe=False,
|
830
|
+
):
|
831
|
+
from ..questions.HTMLQuestion import HTMLQuestion
|
832
|
+
|
833
|
+
return HTMLQuestion(self).html(
|
834
|
+
scenario, agent, answers, include_question_name, height, width, iframe
|
835
|
+
)
|
836
|
+
|
837
|
+
@classmethod
|
838
|
+
def example_model(cls):
|
839
|
+
from ..language_models import Model
|
840
|
+
|
841
|
+
q = cls.example()
|
842
|
+
m = Model("test", canned_response=cls._simulate_answer(q)["answer"])
|
843
|
+
|
844
|
+
return m
|
845
|
+
|
846
|
+
@classmethod
|
847
|
+
def example_results(cls):
|
848
|
+
m = cls.example_model()
|
849
|
+
q = cls.example()
|
850
|
+
return q.by(m).run(cache=False)
|
851
|
+
|
852
|
+
def rich_print(self):
|
853
|
+
"""Print the question in a rich format."""
|
854
|
+
from rich.table import Table
|
855
|
+
|
856
|
+
table = Table(show_header=True, header_style="bold magenta")
|
857
|
+
table.add_column("Question Name", style="dim")
|
858
|
+
table.add_column("Question Type")
|
859
|
+
table.add_column("Question Text")
|
860
|
+
table.add_column("Options")
|
861
|
+
|
862
|
+
question = self
|
863
|
+
if hasattr(question, "question_options"):
|
864
|
+
options = ", ".join([str(o) for o in question.question_options])
|
865
|
+
else:
|
866
|
+
options = "None"
|
867
|
+
table.add_row(
|
868
|
+
question.question_name,
|
869
|
+
question.question_type,
|
870
|
+
question.question_text,
|
871
|
+
options,
|
872
|
+
)
|
873
|
+
return table
|
874
|
+
|
875
|
+
# endregion
|
876
|
+
|
877
|
+
|
878
|
+
if __name__ == "__main__":
|
879
|
+
import doctest
|
880
|
+
|
881
|
+
doctest.testmod(optionflags=doctest.ELLIPSIS)
|