edsl 0.1.46__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 +430 -113
- 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/dataset/dataset_operations_mixin.py +1492 -0
- 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} +12 -12
- 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} +321 -155
- edsl/jobs/{JobsChecks.py → jobs_checks.py} +15 -7
- edsl/jobs/{JobsComponentConstructor.py → jobs_component_constructor.py} +20 -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 +10 -16
- 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} +420 -216
- 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/file_store.py +755 -0
- 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} +424 -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 +20 -21
- 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.46.dist-info → edsl-0.1.48.dist-info}/METADATA +3 -2
- edsl-0.1.48.dist-info/RECORD +347 -0
- edsl/Base.py +0 -426
- edsl/BaseDiff.py +0 -260
- edsl/agents/InvigilatorBase.py +0 -260
- edsl/agents/PromptConstructor.py +0 -318
- edsl/auto/AutoStudy.py +0 -130
- edsl/auto/StageBase.py +0 -243
- edsl/auto/StageGenerateSurvey.py +0 -178
- edsl/auto/StageLabelQuestions.py +0 -125
- edsl/auto/StagePersona.py +0 -61
- edsl/auto/StagePersonaDimensionValueRanges.py +0 -88
- edsl/auto/StagePersonaDimensionValues.py +0 -74
- edsl/auto/StagePersonaDimensions.py +0 -69
- edsl/auto/StageQuestions.py +0 -74
- edsl/auto/SurveyCreatorPipeline.py +0 -21
- edsl/auto/utilities.py +0 -218
- edsl/base/Base.py +0 -279
- edsl/coop/PriceFetcher.py +0 -54
- edsl/data/Cache.py +0 -580
- edsl/data/CacheEntry.py +0 -230
- 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 -539
- edsl/questions/QuestionFreeText.py +0 -130
- edsl/questions/derived/QuestionLikertFive.py +0 -76
- edsl/results/DatasetExportMixin.py +0 -911
- 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/FileStore.py +0 -564
- 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 -1306
- 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.46.dist-info/RECORD +0 -366
- /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.46.dist-info → edsl-0.1.48.dist-info}/LICENSE +0 -0
- {edsl-0.1.46.dist-info → edsl-0.1.48.dist-info}/WHEEL +0 -0
@@ -4,12 +4,11 @@ from typing import Union, Literal, Optional, List, Any
|
|
4
4
|
from jinja2 import Template
|
5
5
|
from pydantic import BaseModel, Field
|
6
6
|
|
7
|
-
from
|
8
|
-
from
|
9
|
-
from
|
10
|
-
from
|
11
|
-
from
|
12
|
-
|
7
|
+
from ..scenarios import Scenario
|
8
|
+
from .question_base import QuestionBase
|
9
|
+
from .descriptors import QuestionOptionsDescriptor
|
10
|
+
from .decorators import inject_exception
|
11
|
+
from .response_validator_abc import ResponseValidatorABC
|
13
12
|
|
14
13
|
def create_response_model(choices: List[str], permissive: bool = False):
|
15
14
|
"""
|
@@ -112,10 +111,64 @@ class MultipleChoiceResponseValidator(ResponseValidatorABC):
|
|
112
111
|
|
113
112
|
|
114
113
|
class QuestionMultipleChoice(QuestionBase):
|
115
|
-
"""
|
116
|
-
|
117
|
-
|
118
|
-
|
114
|
+
"""
|
115
|
+
A question that prompts the agent to select one option from a list of choices.
|
116
|
+
|
117
|
+
QuestionMultipleChoice presents a set of predefined choices to the agent and asks
|
118
|
+
them to select exactly one option. This question type is ideal for scenarios where
|
119
|
+
the possible answers are known and limited, such as surveys, preference questions,
|
120
|
+
or classification tasks.
|
121
|
+
|
122
|
+
Key Features:
|
123
|
+
- Presents a fixed set of options to choose from
|
124
|
+
- Enforces selection of exactly one option
|
125
|
+
- Can use numeric codes for options (use_code=True)
|
126
|
+
- Supports custom instructions and presentation
|
127
|
+
- Optional comment field for additional explanation
|
128
|
+
- Can be configured to be permissive (accept answers outside the options)
|
129
|
+
|
130
|
+
Technical Details:
|
131
|
+
- Uses Pydantic models for validation with Literal types for strict checking
|
132
|
+
- Supports dynamic options from scenario variables
|
133
|
+
- HTML rendering for web interfaces
|
134
|
+
- Robust validation with repair capabilities
|
135
|
+
|
136
|
+
Examples:
|
137
|
+
Basic usage:
|
138
|
+
|
139
|
+
```python
|
140
|
+
q = QuestionMultipleChoice(
|
141
|
+
question_name="preference",
|
142
|
+
question_text="Which color do you prefer?",
|
143
|
+
question_options=["Red", "Green", "Blue", "Yellow"]
|
144
|
+
)
|
145
|
+
```
|
146
|
+
|
147
|
+
With numeric codes:
|
148
|
+
|
149
|
+
```python
|
150
|
+
q = QuestionMultipleChoice(
|
151
|
+
question_name="rating",
|
152
|
+
question_text="Rate this product from 1 to 5",
|
153
|
+
question_options=["Very Poor", "Poor", "Average", "Good", "Excellent"],
|
154
|
+
use_code=True # The answer will be 0-4 instead of the text
|
155
|
+
)
|
156
|
+
```
|
157
|
+
|
158
|
+
Dynamic options from scenario:
|
159
|
+
|
160
|
+
```python
|
161
|
+
q = QuestionMultipleChoice(
|
162
|
+
question_name="choice",
|
163
|
+
question_text="Select an option",
|
164
|
+
question_options=["{{option1}}", "{{option2}}", "{{option3}}"]
|
165
|
+
)
|
166
|
+
scenario = Scenario({"option1": "Choice A", "option2": "Choice B", "option3": "Choice C"})
|
167
|
+
result = q.by(model).with_scenario(scenario).run()
|
168
|
+
```
|
169
|
+
|
170
|
+
See also:
|
171
|
+
https://docs.expectedparrot.com/en/latest/questions.html#questionmultiplechoice-class
|
119
172
|
"""
|
120
173
|
|
121
174
|
question_type = "multiple_choice"
|
@@ -137,17 +190,69 @@ class QuestionMultipleChoice(QuestionBase):
|
|
137
190
|
question_presentation: Optional[str] = None,
|
138
191
|
permissive: bool = False,
|
139
192
|
):
|
140
|
-
"""
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
193
|
+
"""
|
194
|
+
Initialize a new multiple choice question.
|
195
|
+
|
196
|
+
Parameters
|
197
|
+
----------
|
198
|
+
question_name : str
|
199
|
+
The name of the question, used as an identifier. Must be a valid Python variable name.
|
200
|
+
This name will be used in results, templates, and when referencing the question in surveys.
|
201
|
+
|
202
|
+
question_text : str
|
203
|
+
The actual text of the question to be asked. This is the prompt that will be presented
|
204
|
+
to the language model or agent.
|
205
|
+
|
206
|
+
question_options : Union[list[str], list[list], list[float], list[int]]
|
207
|
+
The list of options the agent can select from. These can be:
|
208
|
+
- Strings: ["Option A", "Option B", "Option C"]
|
209
|
+
- Lists: Used for nested or complex options
|
210
|
+
- Numbers: [1, 2, 3, 4, 5] or [0.1, 0.2, 0.3]
|
211
|
+
- Template strings: ["{{var1}}", "{{var2}}"] which will be rendered with scenario variables
|
212
|
+
|
213
|
+
include_comment : bool, default=True
|
214
|
+
Whether to include a comment field in the response, allowing the model to provide
|
215
|
+
additional explanation beyond just selecting an option.
|
216
|
+
|
217
|
+
use_code : bool, default=False
|
218
|
+
If True, the answer will be the index of the selected option (0-based) instead of
|
219
|
+
the option text itself. This is useful for numeric scoring or when option text is long.
|
220
|
+
|
221
|
+
answering_instructions : Optional[str], default=None
|
222
|
+
Custom instructions for how the model should answer the question. If None,
|
223
|
+
default instructions for multiple choice questions will be used.
|
224
|
+
|
225
|
+
question_presentation : Optional[str], default=None
|
226
|
+
Custom template for how the question is presented to the model. If None,
|
227
|
+
the default presentation for multiple choice questions will be used.
|
228
|
+
|
229
|
+
permissive : bool, default=False
|
230
|
+
If True, the validator will accept answers that are not in the provided options list.
|
231
|
+
If False (default), only exact matches to the provided options are allowed.
|
232
|
+
|
233
|
+
Examples
|
234
|
+
--------
|
235
|
+
>>> q = QuestionMultipleChoice(
|
236
|
+
... question_name="color_preference",
|
237
|
+
... question_text="What is your favorite color?",
|
238
|
+
... question_options=["Red", "Blue", "Green", "Yellow"],
|
239
|
+
... include_comment=True
|
240
|
+
... )
|
241
|
+
|
242
|
+
>>> q_numeric = QuestionMultipleChoice(
|
243
|
+
... question_name="rating",
|
244
|
+
... question_text="How would you rate this product?",
|
245
|
+
... question_options=["Very Poor", "Poor", "Average", "Good", "Excellent"],
|
246
|
+
... use_code=True,
|
247
|
+
... include_comment=True
|
248
|
+
... )
|
249
|
+
|
250
|
+
Notes
|
251
|
+
-----
|
252
|
+
- When `use_code=True`, the answer will be the index (0-based) of the selected option
|
253
|
+
- The `permissive` parameter is useful when you want to allow free-form responses
|
254
|
+
while still suggesting options
|
255
|
+
- Dynamic options can reference variables in a scenario using Jinja2 template syntax
|
151
256
|
"""
|
152
257
|
self.question_name = question_name
|
153
258
|
self.question_text = question_text
|
@@ -5,11 +5,11 @@ from typing import Any, Optional, Union, Literal
|
|
5
5
|
|
6
6
|
from pydantic import BaseModel, Field, field_validator
|
7
7
|
|
8
|
-
from
|
9
|
-
from
|
10
|
-
from
|
11
|
-
from
|
12
|
-
from
|
8
|
+
from .exceptions import QuestionAnswerValidationError
|
9
|
+
from .question_base import QuestionBase
|
10
|
+
from .descriptors import NumericalOrNoneDescriptor
|
11
|
+
from .decorators import inject_exception
|
12
|
+
from .response_validator_abc import ResponseValidatorABC
|
13
13
|
|
14
14
|
|
15
15
|
def create_numeric_response(
|
@@ -3,12 +3,13 @@ from typing import Optional, Any, List, Annotated, Literal
|
|
3
3
|
|
4
4
|
from pydantic import BaseModel, Field
|
5
5
|
|
6
|
-
from
|
7
|
-
from
|
6
|
+
from .question_base import QuestionBase
|
7
|
+
from .descriptors import (
|
8
8
|
QuestionOptionsDescriptor,
|
9
9
|
NumSelectionsDescriptor,
|
10
10
|
)
|
11
|
-
from
|
11
|
+
from .response_validator_abc import ResponseValidatorABC
|
12
|
+
from ..scenarios import Scenario
|
12
13
|
|
13
14
|
|
14
15
|
def create_response_model(
|
@@ -190,9 +191,8 @@ class QuestionRank(QuestionBase):
|
|
190
191
|
self, answer_codes, scenario: Scenario = None
|
191
192
|
) -> list[str]:
|
192
193
|
"""Translate the answer code to the actual answer."""
|
193
|
-
from edsl.scenarios.Scenario import Scenario
|
194
194
|
from jinja2 import Template
|
195
|
-
|
195
|
+
|
196
196
|
scenario = scenario or Scenario()
|
197
197
|
translated_options = [
|
198
198
|
Template(option).render(scenario) for option in self.question_options
|
@@ -285,7 +285,7 @@ class QuestionRank(QuestionBase):
|
|
285
285
|
|
286
286
|
def main():
|
287
287
|
"""Show example usage."""
|
288
|
-
from edsl.questions
|
288
|
+
from edsl.questions import QuestionRank
|
289
289
|
|
290
290
|
q = QuestionRank.example(use_code=True)
|
291
291
|
q.question_text
|
@@ -4,8 +4,7 @@ import textwrap
|
|
4
4
|
from uuid import UUID
|
5
5
|
from typing import Any, Optional, Union
|
6
6
|
|
7
|
-
|
8
|
-
from edsl.questions.QuestionBase import RegisterQuestionsMeta
|
7
|
+
from .question_base import RegisterQuestionsMeta
|
9
8
|
|
10
9
|
|
11
10
|
class Meta(type):
|
@@ -60,26 +59,25 @@ class Question(metaclass=Meta):
|
|
60
59
|
return q.example()
|
61
60
|
|
62
61
|
@classmethod
|
63
|
-
def pull(cls,
|
62
|
+
def pull(cls, url_or_uuid: Union[str, UUID]):
|
64
63
|
"""Pull the object from coop."""
|
65
64
|
from edsl.coop import Coop
|
66
65
|
|
67
66
|
coop = Coop()
|
68
|
-
return coop.get(
|
67
|
+
return coop.get(url_or_uuid, "question")
|
69
68
|
|
70
69
|
@classmethod
|
71
|
-
def delete(cls,
|
70
|
+
def delete(cls, url_or_uuid: Union[str, UUID]):
|
72
71
|
"""Delete the object from coop."""
|
73
72
|
from edsl.coop import Coop
|
74
73
|
|
75
74
|
coop = Coop()
|
76
|
-
return coop.delete(
|
75
|
+
return coop.delete(url_or_uuid)
|
77
76
|
|
78
77
|
@classmethod
|
79
78
|
def patch(
|
80
79
|
cls,
|
81
|
-
|
82
|
-
url: Optional[str] = None,
|
80
|
+
url_or_uuid: Union[str, UUID],
|
83
81
|
description: Optional[str] = None,
|
84
82
|
value: Optional[Any] = None,
|
85
83
|
visibility: Optional[str] = None,
|
@@ -88,7 +86,7 @@ class Question(metaclass=Meta):
|
|
88
86
|
from edsl.coop import Coop
|
89
87
|
|
90
88
|
coop = Coop()
|
91
|
-
return coop.patch(
|
89
|
+
return coop.patch(url_or_uuid, description, value, visibility)
|
92
90
|
|
93
91
|
@classmethod
|
94
92
|
def list_question_types(cls):
|
@@ -115,7 +113,7 @@ class Question(metaclass=Meta):
|
|
115
113
|
Example usage:
|
116
114
|
|
117
115
|
"""
|
118
|
-
from
|
116
|
+
from ..dataset import Dataset
|
119
117
|
|
120
118
|
exclude = ["budget"]
|
121
119
|
if show_class_names:
|
@@ -169,9 +167,5 @@ question_purpose = {
|
|
169
167
|
|
170
168
|
|
171
169
|
if __name__ == "__main__":
|
172
|
-
|
173
|
-
|
174
|
-
# q = Question("free_text", question_text="How are you doing?", question_name="test")
|
175
|
-
# results = q.run()
|
176
|
-
|
177
|
-
q = Question.pull(id=76)
|
170
|
+
import doctest
|
171
|
+
doctest.testmod()
|
@@ -1,11 +1,9 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
from abc import ABCMeta
|
3
|
-
|
4
|
-
from edsl.enums import QuestionType
|
5
|
-
from edsl.exceptions.questions import QuestionMissingTypeError, QuestionBadTypeError
|
6
|
-
|
7
3
|
import inspect
|
8
4
|
|
5
|
+
from ..enums import QuestionType
|
6
|
+
from .exceptions import QuestionMissingTypeError, QuestionBadTypeError
|
9
7
|
|
10
8
|
class RegisterQuestionsMeta(ABCMeta):
|
11
9
|
"""Metaclass to register output elements in a registry i.e., those that have a parent."""
|
@@ -69,3 +67,9 @@ class RegisterQuestionsMeta(ABCMeta):
|
|
69
67
|
f"Class {classname} does not have a question_type class attribute"
|
70
68
|
)
|
71
69
|
return d
|
70
|
+
|
71
|
+
|
72
|
+
|
73
|
+
if __name__ == "__main__":
|
74
|
+
import doctest
|
75
|
+
doctest.testmod()
|
@@ -1,15 +1,16 @@
|
|
1
1
|
from abc import ABC, abstractmethod
|
2
|
-
from typing import Optional, Any, List, TypedDict
|
2
|
+
from typing import Optional, Any, List, TypedDict, TYPE_CHECKING
|
3
3
|
|
4
4
|
from pydantic import BaseModel, Field, field_validator, ValidationError
|
5
5
|
|
6
|
-
from
|
7
|
-
from
|
6
|
+
from .exceptions import QuestionAnswerValidationError
|
7
|
+
from .ExceptionExplainer import ExceptionExplainer
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
9
|
+
if TYPE_CHECKING:
|
10
|
+
from edsl.questions.data_structures import (
|
11
|
+
RawEdslAnswerDict,
|
12
|
+
EdslAnswerDict,
|
13
|
+
)
|
13
14
|
|
14
15
|
|
15
16
|
class ResponseValidatorABC(ABC):
|
@@ -53,7 +54,7 @@ class ResponseValidatorABC(ABC):
|
|
53
54
|
|
54
55
|
self.fixes_tried = 0 # how many times we've tried to fix the answer
|
55
56
|
|
56
|
-
def _preprocess(self, data: RawEdslAnswerDict) -> RawEdslAnswerDict:
|
57
|
+
def _preprocess(self, data: 'RawEdslAnswerDict') -> 'RawEdslAnswerDict':
|
57
58
|
"""This is for testing purposes. A question can be given an exception to throw or an answer to always return.
|
58
59
|
|
59
60
|
>>> rv = ResponseValidatorABC.example()
|
@@ -65,7 +66,7 @@ class ResponseValidatorABC(ABC):
|
|
65
66
|
raise self.exception_to_throw
|
66
67
|
return self.override_answer if self.override_answer else data
|
67
68
|
|
68
|
-
def _base_validate(self, data: RawEdslAnswerDict) -> BaseModel:
|
69
|
+
def _base_validate(self, data: 'RawEdslAnswerDict') -> BaseModel:
|
69
70
|
"""This is the main validation function. It takes the response_model and checks the data against it,
|
70
71
|
returning the instantiated model.
|
71
72
|
|
@@ -85,11 +86,11 @@ class ResponseValidatorABC(ABC):
|
|
85
86
|
|
86
87
|
def validate(
|
87
88
|
self,
|
88
|
-
raw_edsl_answer_dict: RawEdslAnswerDict,
|
89
|
+
raw_edsl_answer_dict: 'RawEdslAnswerDict',
|
89
90
|
fix=False,
|
90
91
|
verbose=False,
|
91
92
|
replacement_dict: dict = None,
|
92
|
-
) -> EdslAnswerDict:
|
93
|
+
) -> 'EdslAnswerDict':
|
93
94
|
"""This is the main validation function.
|
94
95
|
|
95
96
|
>>> rv = ResponseValidatorABC.example("numerical")
|
@@ -100,7 +101,7 @@ class ResponseValidatorABC(ABC):
|
|
100
101
|
>>> rv.validate({"answer": "120"})
|
101
102
|
Traceback (most recent call last):
|
102
103
|
...
|
103
|
-
edsl.exceptions.
|
104
|
+
edsl.questions.exceptions.QuestionAnswerValidationError:...
|
104
105
|
>>> from edsl import QuestionNumerical
|
105
106
|
>>> q = QuestionNumerical.example()
|
106
107
|
>>> q.permissive = True
|
@@ -110,7 +111,7 @@ class ResponseValidatorABC(ABC):
|
|
110
111
|
>>> rv.validate({"answer": "poo"})
|
111
112
|
Traceback (most recent call last):
|
112
113
|
...
|
113
|
-
edsl.exceptions.
|
114
|
+
edsl.questions.exceptions.QuestionAnswerValidationError:...
|
114
115
|
"""
|
115
116
|
proposed_edsl_answer_dict = self._preprocess(raw_edsl_answer_dict)
|
116
117
|
try:
|
@@ -126,7 +127,7 @@ class ResponseValidatorABC(ABC):
|
|
126
127
|
explanation = ExceptionExplainer(e, model_response=e.data).explain()
|
127
128
|
return explanation
|
128
129
|
|
129
|
-
def _handle_exception(self, e: Exception, raw_edsl_answer_dict) -> EdslAnswerDict:
|
130
|
+
def _handle_exception(self, e: Exception, raw_edsl_answer_dict) -> 'EdslAnswerDict':
|
130
131
|
if self.fixes_tried == 0:
|
131
132
|
self.original_exception = e
|
132
133
|
|
@@ -153,10 +154,10 @@ class ResponseValidatorABC(ABC):
|
|
153
154
|
def _check_constraints(self, pydantic_edsl_answer: BaseModel) -> dict:
|
154
155
|
pass
|
155
156
|
|
156
|
-
def _extract_answer(self, response: BaseModel) -> EdslAnswerDict:
|
157
|
+
def _extract_answer(self, response: BaseModel) -> 'EdslAnswerDict':
|
157
158
|
return response.model_dump()
|
158
159
|
|
159
|
-
def _post_process(self, edsl_answer_dict: EdslAnswerDict) -> EdslAnswerDict:
|
160
|
+
def _post_process(self, edsl_answer_dict: 'EdslAnswerDict') -> 'EdslAnswerDict':
|
160
161
|
return edsl_answer_dict
|
161
162
|
|
162
163
|
@classmethod
|
edsl/results/__init__.py
CHANGED
edsl/results/report.py
ADDED
@@ -0,0 +1,197 @@
|
|
1
|
+
import jinja2
|
2
|
+
import textwrap
|
3
|
+
import warnings
|
4
|
+
|
5
|
+
class Report:
|
6
|
+
"""
|
7
|
+
A flexible report generator for creating formatted output from EDSL datasets.
|
8
|
+
|
9
|
+
The Report class provides a powerful yet simple way to create customized reports
|
10
|
+
from your survey results. It uses Jinja2 templates to format the data with complete
|
11
|
+
control over the presentation. This is particularly useful for:
|
12
|
+
|
13
|
+
- Creating human-readable summaries of your results
|
14
|
+
- Generating standardized reports for stakeholders
|
15
|
+
- Formatting results for inclusion in papers or presentations
|
16
|
+
- Creating custom visualizations of your data
|
17
|
+
|
18
|
+
The Report class works with any object that supports the Dataset interface,
|
19
|
+
including Results objects after using the select() method.
|
20
|
+
|
21
|
+
Key features:
|
22
|
+
|
23
|
+
- Flexible templating with full Jinja2 syntax
|
24
|
+
- Support for filtering and sorting data
|
25
|
+
- Customizable field selection and labeling
|
26
|
+
- Simple integration with Results and Dataset objects
|
27
|
+
|
28
|
+
Usage:
|
29
|
+
report = Report(
|
30
|
+
dataset=my_dataset,
|
31
|
+
fields=["answer.how_feeling", "answer.how_feeling_yesterday"],
|
32
|
+
template=textwrap.dedent(\"\"\"\
|
33
|
+
# Observation {{ i }}
|
34
|
+
How feeling: {{ row['answer.how_feeling'] }}
|
35
|
+
How feeling yesterday: {{ row['answer.how_feeling_yesterday'] }}
|
36
|
+
|
37
|
+
---
|
38
|
+
\"\"\")
|
39
|
+
)
|
40
|
+
print(report.generate())
|
41
|
+
"""
|
42
|
+
def __init__(
|
43
|
+
self,
|
44
|
+
dataset,
|
45
|
+
fields=None,
|
46
|
+
template=None,
|
47
|
+
top_n=None,
|
48
|
+
pretty_labels=None,
|
49
|
+
filter_func=None,
|
50
|
+
sort_by=None
|
51
|
+
):
|
52
|
+
"""
|
53
|
+
:param dataset: The Dataset instance (DatasetExportMixin-based) to report on.
|
54
|
+
:param fields: List of fields (column names) to include in the report. If None, use all.
|
55
|
+
:param template: A Jinja2-compatible template string describing how each row should be rendered.
|
56
|
+
Within the template, you have access to:
|
57
|
+
- {{ i }} (the 1-based index of the row)
|
58
|
+
- {{ row }} (dictionary of field values for that row)
|
59
|
+
:param top_n: If provided, only report on the first N observations.
|
60
|
+
:param pretty_labels: Dict mapping original field names to "pretty" labels used inside the template,
|
61
|
+
or you can manually handle that in the template yourself.
|
62
|
+
:param filter_func: Optional callable(row_dict) -> bool. If given, only rows for which
|
63
|
+
filter_func(row_dict) is True will appear in the final report.
|
64
|
+
:param sort_by: Optional single field name or list of field names to sort by.
|
65
|
+
"""
|
66
|
+
self.dataset = dataset
|
67
|
+
self.fields = fields
|
68
|
+
self.template = template
|
69
|
+
self.top_n = top_n
|
70
|
+
self.pretty_labels = pretty_labels or {}
|
71
|
+
self.filter_func = filter_func
|
72
|
+
self.sort_by = sort_by
|
73
|
+
|
74
|
+
# Provide a simple fallback template
|
75
|
+
if not self.template:
|
76
|
+
# A minimal default: print all fields line by line
|
77
|
+
# with a heading "Observation #1" etc.
|
78
|
+
self.template = textwrap.dedent("""\
|
79
|
+
# Observation {{ i }}
|
80
|
+
{% for key, value in row.items() %}
|
81
|
+
**{{ key }}**: {{ value }}
|
82
|
+
{% endfor %}
|
83
|
+
---
|
84
|
+
""")
|
85
|
+
|
86
|
+
def _prepare_data(self):
|
87
|
+
"""
|
88
|
+
Convert dataset into a list of dictionaries (one per row),
|
89
|
+
optionally filtering, sorting, and limiting to top_n rows.
|
90
|
+
"""
|
91
|
+
# 1) Decide which fields to include
|
92
|
+
if not self.fields:
|
93
|
+
self.fields = self.dataset.relevant_columns()
|
94
|
+
|
95
|
+
# 2) Convert to list of dictionaries
|
96
|
+
# removing prefix because we typically want "field" instead of "answer.field"
|
97
|
+
data_dicts = self.dataset.to_dicts(remove_prefix=False)
|
98
|
+
|
99
|
+
# 3) Filter out any rows if filter_func is given
|
100
|
+
if self.filter_func:
|
101
|
+
data_dicts = [row for row in data_dicts if self.filter_func(row)]
|
102
|
+
|
103
|
+
# 4) If sort_by was specified, we’ll do a simple sort
|
104
|
+
if self.sort_by:
|
105
|
+
if isinstance(self.sort_by, str):
|
106
|
+
sort_keys = [self.sort_by]
|
107
|
+
else:
|
108
|
+
sort_keys = self.sort_by
|
109
|
+
|
110
|
+
# Python's sort can't directly do multi-key with fields from a dict
|
111
|
+
# unless we do something like tuple(...) for each field
|
112
|
+
# For simplicity, do a stable sort in reverse order for multiple keys
|
113
|
+
# or do a single pass with a tuple:
|
114
|
+
data_dicts.sort(key=lambda row: tuple(row.get(k) for k in sort_keys))
|
115
|
+
|
116
|
+
# 5) If top_n is specified, slice
|
117
|
+
if self.top_n is not None:
|
118
|
+
data_dicts = data_dicts[: self.top_n]
|
119
|
+
|
120
|
+
# 6) Optionally rename fields if pretty_labels is given
|
121
|
+
# (But typically you'd use it inside the template. This is just an example.)
|
122
|
+
if self.pretty_labels:
|
123
|
+
# We'll apply them in a copy so the original keys are still accessible
|
124
|
+
data_for_report = []
|
125
|
+
for row in data_dicts:
|
126
|
+
# copy of the row, but with replaced keys
|
127
|
+
new_row = {}
|
128
|
+
for k, v in row.items():
|
129
|
+
display_key = self.pretty_labels.get(k, k)
|
130
|
+
new_row[display_key] = v
|
131
|
+
data_for_report.append(new_row)
|
132
|
+
data_dicts = data_for_report
|
133
|
+
|
134
|
+
return data_dicts
|
135
|
+
|
136
|
+
def generate(self) -> str:
|
137
|
+
"""
|
138
|
+
Render the final report as a string.
|
139
|
+
|
140
|
+
This method applies the Jinja2 template to each row of data and
|
141
|
+
combines the results into a single string. The template has access
|
142
|
+
to the row index (i) and the row data (row) for each observation.
|
143
|
+
|
144
|
+
Returns:
|
145
|
+
A formatted string containing the complete report.
|
146
|
+
|
147
|
+
Examples:
|
148
|
+
>>> from edsl import Results
|
149
|
+
>>> ds = Results.example().select("how_feeling")
|
150
|
+
>>> report = Report(dataset=ds)
|
151
|
+
>>> lines = report.generate().split("\\n")
|
152
|
+
>>> lines[0]
|
153
|
+
'# Observation 1'
|
154
|
+
|
155
|
+
>>> # Custom template
|
156
|
+
>>> template = "Row {{ i }}: {{ row['answer.how_feeling'] }}\\n"
|
157
|
+
>>> report = Report(dataset=ds, template=template)
|
158
|
+
>>> report.generate().split("\\n")[0]
|
159
|
+
'Row 1: OK'
|
160
|
+
"""
|
161
|
+
# Prepare data
|
162
|
+
data_dicts = self._prepare_data()
|
163
|
+
|
164
|
+
# Build a single Jinja2 template
|
165
|
+
template_obj = jinja2.Template(self.template)
|
166
|
+
|
167
|
+
output = []
|
168
|
+
for i, row in enumerate(data_dicts, start=1):
|
169
|
+
rendered = template_obj.render(i=i, row=row)
|
170
|
+
output.append(rendered.strip())
|
171
|
+
|
172
|
+
return "\n\n".join(output)
|
173
|
+
|
174
|
+
|
175
|
+
if __name__ == "__main__":
|
176
|
+
# Suppose you have an existing Dataset
|
177
|
+
from edsl import Results
|
178
|
+
ds = Results.example().select("how_feeling", "how_feeling_yesterday")
|
179
|
+
|
180
|
+
# Provide a custom template string
|
181
|
+
my_template = textwrap.dedent("""\
|
182
|
+
## Row {{ i }}
|
183
|
+
|
184
|
+
Feeling: {{ row['answer.how_feeling'] }}
|
185
|
+
Yesterday: {{ row['answer.how_feeling_yesterday'] }}
|
186
|
+
|
187
|
+
--------------------
|
188
|
+
""")
|
189
|
+
|
190
|
+
report = Report(
|
191
|
+
dataset=ds,
|
192
|
+
fields=["answer.how_feeling", "answer.how_feeling_yesterday"],
|
193
|
+
template=my_template,
|
194
|
+
top_n=3, # only the first 3 observations
|
195
|
+
)
|
196
|
+
|
197
|
+
print(report.generate())
|