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.
Files changed (328) hide show
  1. edsl/__init__.py +44 -39
  2. edsl/__version__.py +1 -1
  3. edsl/agents/__init__.py +4 -2
  4. edsl/agents/{Agent.py → agent.py} +442 -152
  5. edsl/agents/{AgentList.py → agent_list.py} +220 -162
  6. edsl/agents/descriptors.py +46 -7
  7. edsl/{exceptions/agents.py → agents/exceptions.py} +3 -12
  8. edsl/base/__init__.py +75 -0
  9. edsl/base/base_class.py +1303 -0
  10. edsl/base/data_transfer_models.py +114 -0
  11. edsl/base/enums.py +215 -0
  12. edsl/base.py +8 -0
  13. edsl/buckets/__init__.py +25 -0
  14. edsl/buckets/bucket_collection.py +324 -0
  15. edsl/buckets/model_buckets.py +206 -0
  16. edsl/buckets/token_bucket.py +502 -0
  17. edsl/{jobs/buckets/TokenBucketAPI.py → buckets/token_bucket_api.py} +1 -1
  18. edsl/buckets/token_bucket_client.py +509 -0
  19. edsl/caching/__init__.py +20 -0
  20. edsl/caching/cache.py +814 -0
  21. edsl/caching/cache_entry.py +427 -0
  22. edsl/{data/CacheHandler.py → caching/cache_handler.py} +14 -15
  23. edsl/caching/exceptions.py +24 -0
  24. edsl/caching/orm.py +30 -0
  25. edsl/{data/RemoteCacheSync.py → caching/remote_cache_sync.py} +3 -3
  26. edsl/caching/sql_dict.py +441 -0
  27. edsl/config/__init__.py +8 -0
  28. edsl/config/config_class.py +177 -0
  29. edsl/config.py +4 -176
  30. edsl/conversation/Conversation.py +7 -7
  31. edsl/conversation/car_buying.py +4 -4
  32. edsl/conversation/chips.py +6 -6
  33. edsl/coop/__init__.py +25 -2
  34. edsl/coop/coop.py +430 -113
  35. edsl/coop/{ExpectedParrotKeyHandler.py → ep_key_handling.py} +86 -10
  36. edsl/coop/exceptions.py +62 -0
  37. edsl/coop/price_fetcher.py +126 -0
  38. edsl/coop/utils.py +89 -24
  39. edsl/data_transfer_models.py +5 -72
  40. edsl/dataset/__init__.py +10 -0
  41. edsl/{results/Dataset.py → dataset/dataset.py} +116 -36
  42. edsl/dataset/dataset_operations_mixin.py +1492 -0
  43. edsl/{results/DatasetTree.py → dataset/dataset_tree.py} +156 -75
  44. edsl/{results/TableDisplay.py → dataset/display/table_display.py} +18 -7
  45. edsl/{results → dataset/display}/table_renderers.py +58 -2
  46. edsl/{results → dataset}/file_exports.py +4 -5
  47. edsl/{results → dataset}/smart_objects.py +2 -2
  48. edsl/enums.py +5 -205
  49. edsl/inference_services/__init__.py +5 -0
  50. edsl/inference_services/{AvailableModelCacheHandler.py → available_model_cache_handler.py} +2 -3
  51. edsl/inference_services/{AvailableModelFetcher.py → available_model_fetcher.py} +8 -14
  52. edsl/inference_services/data_structures.py +3 -2
  53. edsl/{exceptions/inference_services.py → inference_services/exceptions.py} +1 -1
  54. edsl/inference_services/{InferenceServiceABC.py → inference_service_abc.py} +1 -1
  55. edsl/inference_services/{InferenceServicesCollection.py → inference_services_collection.py} +8 -7
  56. edsl/inference_services/registry.py +4 -41
  57. edsl/inference_services/{ServiceAvailability.py → service_availability.py} +5 -25
  58. edsl/inference_services/services/__init__.py +31 -0
  59. edsl/inference_services/{AnthropicService.py → services/anthropic_service.py} +3 -3
  60. edsl/inference_services/{AwsBedrock.py → services/aws_bedrock.py} +2 -2
  61. edsl/inference_services/{AzureAI.py → services/azure_ai.py} +2 -2
  62. edsl/inference_services/{DeepInfraService.py → services/deep_infra_service.py} +1 -3
  63. edsl/inference_services/{DeepSeekService.py → services/deep_seek_service.py} +2 -4
  64. edsl/inference_services/{GoogleService.py → services/google_service.py} +5 -4
  65. edsl/inference_services/{GroqService.py → services/groq_service.py} +1 -1
  66. edsl/inference_services/{MistralAIService.py → services/mistral_ai_service.py} +3 -3
  67. edsl/inference_services/{OllamaService.py → services/ollama_service.py} +1 -7
  68. edsl/inference_services/{OpenAIService.py → services/open_ai_service.py} +5 -6
  69. edsl/inference_services/{PerplexityService.py → services/perplexity_service.py} +12 -12
  70. edsl/inference_services/{TestService.py → services/test_service.py} +7 -6
  71. edsl/inference_services/{TogetherAIService.py → services/together_ai_service.py} +2 -6
  72. edsl/inference_services/{XAIService.py → services/xai_service.py} +1 -1
  73. edsl/inference_services/write_available.py +1 -2
  74. edsl/instructions/__init__.py +6 -0
  75. edsl/{surveys/instructions/Instruction.py → instructions/instruction.py} +11 -6
  76. edsl/{surveys/instructions/InstructionCollection.py → instructions/instruction_collection.py} +10 -5
  77. edsl/{surveys/InstructionHandler.py → instructions/instruction_handler.py} +3 -3
  78. edsl/{jobs/interviews → interviews}/ReportErrors.py +2 -2
  79. edsl/interviews/__init__.py +4 -0
  80. edsl/{jobs/AnswerQuestionFunctionConstructor.py → interviews/answering_function.py} +45 -18
  81. edsl/{jobs/interviews/InterviewExceptionEntry.py → interviews/exception_tracking.py} +107 -22
  82. edsl/interviews/interview.py +638 -0
  83. edsl/{jobs/interviews/InterviewStatusDictionary.py → interviews/interview_status_dictionary.py} +21 -12
  84. edsl/{jobs/interviews/InterviewStatusLog.py → interviews/interview_status_log.py} +16 -7
  85. edsl/{jobs/InterviewTaskManager.py → interviews/interview_task_manager.py} +12 -7
  86. edsl/{jobs/RequestTokenEstimator.py → interviews/request_token_estimator.py} +8 -3
  87. edsl/{jobs/interviews/InterviewStatistic.py → interviews/statistics.py} +36 -10
  88. edsl/invigilators/__init__.py +38 -0
  89. edsl/invigilators/invigilator_base.py +477 -0
  90. edsl/{agents/Invigilator.py → invigilators/invigilators.py} +263 -10
  91. edsl/invigilators/prompt_constructor.py +476 -0
  92. edsl/{agents → invigilators}/prompt_helpers.py +2 -1
  93. edsl/{agents/QuestionInstructionPromptBuilder.py → invigilators/question_instructions_prompt_builder.py} +18 -13
  94. edsl/{agents → invigilators}/question_option_processor.py +96 -21
  95. edsl/{agents/QuestionTemplateReplacementsBuilder.py → invigilators/question_template_replacements_builder.py} +64 -12
  96. edsl/jobs/__init__.py +7 -1
  97. edsl/jobs/async_interview_runner.py +99 -35
  98. edsl/jobs/check_survey_scenario_compatibility.py +7 -5
  99. edsl/jobs/data_structures.py +153 -22
  100. edsl/{exceptions/jobs.py → jobs/exceptions.py} +2 -1
  101. edsl/jobs/{FetchInvigilator.py → fetch_invigilator.py} +4 -4
  102. edsl/jobs/{loggers/HTMLTableJobLogger.py → html_table_job_logger.py} +6 -2
  103. edsl/jobs/{Jobs.py → jobs.py} +321 -155
  104. edsl/jobs/{JobsChecks.py → jobs_checks.py} +15 -7
  105. edsl/jobs/{JobsComponentConstructor.py → jobs_component_constructor.py} +20 -17
  106. edsl/jobs/{InterviewsConstructor.py → jobs_interview_constructor.py} +10 -5
  107. edsl/jobs/jobs_pricing_estimation.py +347 -0
  108. edsl/jobs/{JobsRemoteInferenceLogger.py → jobs_remote_inference_logger.py} +4 -3
  109. edsl/jobs/jobs_runner_asyncio.py +282 -0
  110. edsl/jobs/{JobsRemoteInferenceHandler.py → remote_inference.py} +19 -22
  111. edsl/jobs/results_exceptions_handler.py +2 -2
  112. edsl/key_management/__init__.py +28 -0
  113. edsl/key_management/key_lookup.py +161 -0
  114. edsl/{language_models/key_management/KeyLookupBuilder.py → key_management/key_lookup_builder.py} +118 -47
  115. edsl/key_management/key_lookup_collection.py +82 -0
  116. edsl/key_management/models.py +218 -0
  117. edsl/language_models/__init__.py +7 -2
  118. edsl/language_models/{ComputeCost.py → compute_cost.py} +18 -3
  119. edsl/{exceptions/language_models.py → language_models/exceptions.py} +2 -1
  120. edsl/language_models/language_model.py +1080 -0
  121. edsl/language_models/model.py +10 -25
  122. edsl/language_models/{ModelList.py → model_list.py} +9 -14
  123. edsl/language_models/{RawResponseHandler.py → raw_response_handler.py} +1 -1
  124. edsl/language_models/{RegisterLanguageModelsMeta.py → registry.py} +1 -1
  125. edsl/language_models/repair.py +4 -4
  126. edsl/language_models/utilities.py +4 -4
  127. edsl/notebooks/__init__.py +3 -1
  128. edsl/notebooks/{Notebook.py → notebook.py} +7 -8
  129. edsl/prompts/__init__.py +1 -1
  130. edsl/{exceptions/prompts.py → prompts/exceptions.py} +3 -1
  131. edsl/prompts/{Prompt.py → prompt.py} +101 -95
  132. edsl/questions/HTMLQuestion.py +1 -1
  133. edsl/questions/__init__.py +154 -25
  134. edsl/questions/answer_validator_mixin.py +1 -1
  135. edsl/questions/compose_questions.py +4 -3
  136. edsl/questions/derived/question_likert_five.py +166 -0
  137. edsl/questions/derived/{QuestionLinearScale.py → question_linear_scale.py} +4 -4
  138. edsl/questions/derived/{QuestionTopK.py → question_top_k.py} +4 -4
  139. edsl/questions/derived/{QuestionYesNo.py → question_yes_no.py} +4 -5
  140. edsl/questions/descriptors.py +24 -30
  141. edsl/questions/loop_processor.py +65 -19
  142. edsl/questions/question_base.py +881 -0
  143. edsl/questions/question_base_gen_mixin.py +15 -16
  144. edsl/questions/{QuestionBasePromptsMixin.py → question_base_prompts_mixin.py} +2 -2
  145. edsl/questions/{QuestionBudget.py → question_budget.py} +3 -4
  146. edsl/questions/{QuestionCheckBox.py → question_check_box.py} +16 -16
  147. edsl/questions/{QuestionDict.py → question_dict.py} +39 -5
  148. edsl/questions/{QuestionExtract.py → question_extract.py} +9 -9
  149. edsl/questions/question_free_text.py +282 -0
  150. edsl/questions/{QuestionFunctional.py → question_functional.py} +6 -5
  151. edsl/questions/{QuestionList.py → question_list.py} +6 -7
  152. edsl/questions/{QuestionMatrix.py → question_matrix.py} +6 -5
  153. edsl/questions/{QuestionMultipleChoice.py → question_multiple_choice.py} +126 -21
  154. edsl/questions/{QuestionNumerical.py → question_numerical.py} +5 -5
  155. edsl/questions/{QuestionRank.py → question_rank.py} +6 -6
  156. edsl/questions/question_registry.py +10 -16
  157. edsl/questions/register_questions_meta.py +8 -4
  158. edsl/questions/response_validator_abc.py +17 -16
  159. edsl/results/__init__.py +4 -1
  160. edsl/{exceptions/results.py → results/exceptions.py} +1 -1
  161. edsl/results/report.py +197 -0
  162. edsl/results/{Result.py → result.py} +131 -45
  163. edsl/results/{Results.py → results.py} +420 -216
  164. edsl/results/results_selector.py +344 -25
  165. edsl/scenarios/__init__.py +30 -3
  166. edsl/scenarios/{ConstructDownloadLink.py → construct_download_link.py} +7 -0
  167. edsl/scenarios/directory_scanner.py +156 -13
  168. edsl/scenarios/document_chunker.py +186 -0
  169. edsl/scenarios/exceptions.py +101 -0
  170. edsl/scenarios/file_methods.py +2 -3
  171. edsl/scenarios/file_store.py +755 -0
  172. edsl/scenarios/handlers/__init__.py +14 -14
  173. edsl/scenarios/handlers/{csv.py → csv_file_store.py} +1 -2
  174. edsl/scenarios/handlers/{docx.py → docx_file_store.py} +8 -7
  175. edsl/scenarios/handlers/{html.py → html_file_store.py} +1 -2
  176. edsl/scenarios/handlers/{jpeg.py → jpeg_file_store.py} +1 -1
  177. edsl/scenarios/handlers/{json.py → json_file_store.py} +1 -1
  178. edsl/scenarios/handlers/latex_file_store.py +5 -0
  179. edsl/scenarios/handlers/{md.py → md_file_store.py} +1 -1
  180. edsl/scenarios/handlers/{pdf.py → pdf_file_store.py} +2 -2
  181. edsl/scenarios/handlers/{png.py → png_file_store.py} +1 -1
  182. edsl/scenarios/handlers/{pptx.py → pptx_file_store.py} +8 -7
  183. edsl/scenarios/handlers/{py.py → py_file_store.py} +1 -3
  184. edsl/scenarios/handlers/{sql.py → sql_file_store.py} +2 -1
  185. edsl/scenarios/handlers/{sqlite.py → sqlite_file_store.py} +2 -3
  186. edsl/scenarios/handlers/{txt.py → txt_file_store.py} +1 -1
  187. edsl/scenarios/scenario.py +928 -0
  188. edsl/scenarios/scenario_join.py +18 -5
  189. edsl/scenarios/{ScenarioList.py → scenario_list.py} +424 -106
  190. edsl/scenarios/{ScenarioListPdfMixin.py → scenario_list_pdf_tools.py} +16 -15
  191. edsl/scenarios/scenario_selector.py +5 -1
  192. edsl/study/ObjectEntry.py +2 -2
  193. edsl/study/SnapShot.py +5 -5
  194. edsl/study/Study.py +20 -21
  195. edsl/study/__init__.py +6 -4
  196. edsl/surveys/__init__.py +7 -4
  197. edsl/surveys/dag/__init__.py +2 -0
  198. edsl/surveys/{ConstructDAG.py → dag/construct_dag.py} +3 -3
  199. edsl/surveys/{DAG.py → dag/dag.py} +13 -10
  200. edsl/surveys/descriptors.py +1 -1
  201. edsl/surveys/{EditSurvey.py → edit_survey.py} +9 -9
  202. edsl/{exceptions/surveys.py → surveys/exceptions.py} +1 -2
  203. edsl/surveys/memory/__init__.py +3 -0
  204. edsl/surveys/{MemoryPlan.py → memory/memory_plan.py} +10 -9
  205. edsl/surveys/rules/__init__.py +3 -0
  206. edsl/surveys/{Rule.py → rules/rule.py} +103 -43
  207. edsl/surveys/{RuleCollection.py → rules/rule_collection.py} +21 -30
  208. edsl/surveys/{RuleManager.py → rules/rule_manager.py} +19 -13
  209. edsl/surveys/survey.py +1743 -0
  210. edsl/surveys/{SurveyExportMixin.py → survey_export.py} +22 -27
  211. edsl/surveys/{SurveyFlowVisualization.py → survey_flow_visualization.py} +11 -2
  212. edsl/surveys/{Simulator.py → survey_simulator.py} +10 -3
  213. edsl/tasks/__init__.py +32 -0
  214. edsl/{jobs/tasks/QuestionTaskCreator.py → tasks/question_task_creator.py} +115 -57
  215. edsl/tasks/task_creators.py +135 -0
  216. edsl/{jobs/tasks/TaskHistory.py → tasks/task_history.py} +86 -47
  217. edsl/{jobs/tasks → tasks}/task_status_enum.py +91 -7
  218. edsl/tasks/task_status_log.py +85 -0
  219. edsl/tokens/__init__.py +2 -0
  220. edsl/tokens/interview_token_usage.py +53 -0
  221. edsl/utilities/PrettyList.py +1 -1
  222. edsl/utilities/SystemInfo.py +25 -22
  223. edsl/utilities/__init__.py +29 -21
  224. edsl/utilities/gcp_bucket/__init__.py +2 -0
  225. edsl/utilities/gcp_bucket/cloud_storage.py +99 -96
  226. edsl/utilities/interface.py +44 -536
  227. edsl/{results/MarkdownToPDF.py → utilities/markdown_to_pdf.py} +13 -5
  228. edsl/utilities/repair_functions.py +1 -1
  229. {edsl-0.1.46.dist-info → edsl-0.1.48.dist-info}/METADATA +3 -2
  230. edsl-0.1.48.dist-info/RECORD +347 -0
  231. edsl/Base.py +0 -426
  232. edsl/BaseDiff.py +0 -260
  233. edsl/agents/InvigilatorBase.py +0 -260
  234. edsl/agents/PromptConstructor.py +0 -318
  235. edsl/auto/AutoStudy.py +0 -130
  236. edsl/auto/StageBase.py +0 -243
  237. edsl/auto/StageGenerateSurvey.py +0 -178
  238. edsl/auto/StageLabelQuestions.py +0 -125
  239. edsl/auto/StagePersona.py +0 -61
  240. edsl/auto/StagePersonaDimensionValueRanges.py +0 -88
  241. edsl/auto/StagePersonaDimensionValues.py +0 -74
  242. edsl/auto/StagePersonaDimensions.py +0 -69
  243. edsl/auto/StageQuestions.py +0 -74
  244. edsl/auto/SurveyCreatorPipeline.py +0 -21
  245. edsl/auto/utilities.py +0 -218
  246. edsl/base/Base.py +0 -279
  247. edsl/coop/PriceFetcher.py +0 -54
  248. edsl/data/Cache.py +0 -580
  249. edsl/data/CacheEntry.py +0 -230
  250. edsl/data/SQLiteDict.py +0 -292
  251. edsl/data/__init__.py +0 -5
  252. edsl/data/orm.py +0 -10
  253. edsl/exceptions/cache.py +0 -5
  254. edsl/exceptions/coop.py +0 -14
  255. edsl/exceptions/data.py +0 -14
  256. edsl/exceptions/scenarios.py +0 -29
  257. edsl/jobs/Answers.py +0 -43
  258. edsl/jobs/JobsPrompts.py +0 -354
  259. edsl/jobs/buckets/BucketCollection.py +0 -134
  260. edsl/jobs/buckets/ModelBuckets.py +0 -65
  261. edsl/jobs/buckets/TokenBucket.py +0 -283
  262. edsl/jobs/buckets/TokenBucketClient.py +0 -191
  263. edsl/jobs/interviews/Interview.py +0 -395
  264. edsl/jobs/interviews/InterviewExceptionCollection.py +0 -99
  265. edsl/jobs/interviews/InterviewStatisticsCollection.py +0 -25
  266. edsl/jobs/runners/JobsRunnerAsyncio.py +0 -163
  267. edsl/jobs/runners/JobsRunnerStatusData.py +0 -0
  268. edsl/jobs/tasks/TaskCreators.py +0 -64
  269. edsl/jobs/tasks/TaskStatusLog.py +0 -23
  270. edsl/jobs/tokens/InterviewTokenUsage.py +0 -27
  271. edsl/language_models/LanguageModel.py +0 -635
  272. edsl/language_models/ServiceDataSources.py +0 -0
  273. edsl/language_models/key_management/KeyLookup.py +0 -63
  274. edsl/language_models/key_management/KeyLookupCollection.py +0 -38
  275. edsl/language_models/key_management/models.py +0 -137
  276. edsl/questions/QuestionBase.py +0 -539
  277. edsl/questions/QuestionFreeText.py +0 -130
  278. edsl/questions/derived/QuestionLikertFive.py +0 -76
  279. edsl/results/DatasetExportMixin.py +0 -911
  280. edsl/results/ResultsExportMixin.py +0 -45
  281. edsl/results/TextEditor.py +0 -50
  282. edsl/results/results_fetch_mixin.py +0 -33
  283. edsl/results/results_tools_mixin.py +0 -98
  284. edsl/scenarios/DocumentChunker.py +0 -104
  285. edsl/scenarios/FileStore.py +0 -564
  286. edsl/scenarios/Scenario.py +0 -548
  287. edsl/scenarios/ScenarioHtmlMixin.py +0 -65
  288. edsl/scenarios/ScenarioListExportMixin.py +0 -45
  289. edsl/scenarios/handlers/latex.py +0 -5
  290. edsl/shared.py +0 -1
  291. edsl/surveys/Survey.py +0 -1306
  292. edsl/surveys/SurveyQualtricsImport.py +0 -284
  293. edsl/surveys/SurveyToApp.py +0 -141
  294. edsl/surveys/instructions/__init__.py +0 -0
  295. edsl/tools/__init__.py +0 -1
  296. edsl/tools/clusters.py +0 -192
  297. edsl/tools/embeddings.py +0 -27
  298. edsl/tools/embeddings_plotting.py +0 -118
  299. edsl/tools/plotting.py +0 -112
  300. edsl/tools/summarize.py +0 -18
  301. edsl/utilities/data/Registry.py +0 -6
  302. edsl/utilities/data/__init__.py +0 -1
  303. edsl/utilities/data/scooter_results.json +0 -1
  304. edsl-0.1.46.dist-info/RECORD +0 -366
  305. /edsl/coop/{CoopFunctionsMixin.py → coop_functions.py} +0 -0
  306. /edsl/{results → dataset/display}/CSSParameterizer.py +0 -0
  307. /edsl/{language_models/key_management → dataset/display}/__init__.py +0 -0
  308. /edsl/{results → dataset/display}/table_data_class.py +0 -0
  309. /edsl/{results → dataset/display}/table_display.css +0 -0
  310. /edsl/{results/ResultsGGMixin.py → dataset/r/ggplot.py} +0 -0
  311. /edsl/{results → dataset}/tree_explore.py +0 -0
  312. /edsl/{surveys/instructions/ChangeInstruction.py → instructions/change_instruction.py} +0 -0
  313. /edsl/{jobs/interviews → interviews}/interview_status_enum.py +0 -0
  314. /edsl/jobs/{runners/JobsRunnerStatus.py → jobs_runner_status.py} +0 -0
  315. /edsl/language_models/{PriceManager.py → price_manager.py} +0 -0
  316. /edsl/language_models/{fake_openai_call.py → unused/fake_openai_call.py} +0 -0
  317. /edsl/language_models/{fake_openai_service.py → unused/fake_openai_service.py} +0 -0
  318. /edsl/notebooks/{NotebookToLaTeX.py → notebook_to_latex.py} +0 -0
  319. /edsl/{exceptions/questions.py → questions/exceptions.py} +0 -0
  320. /edsl/questions/{SimpleAskMixin.py → simple_ask_mixin.py} +0 -0
  321. /edsl/surveys/{Memory.py → memory/memory.py} +0 -0
  322. /edsl/surveys/{MemoryManagement.py → memory/memory_management.py} +0 -0
  323. /edsl/surveys/{SurveyCSS.py → survey_css.py} +0 -0
  324. /edsl/{jobs/tokens/TokenUsage.py → tokens/token_usage.py} +0 -0
  325. /edsl/{results/MarkdownToDocx.py → utilities/markdown_to_docx.py} +0 -0
  326. /edsl/{TemplateLoader.py → utilities/template_loader.py} +0 -0
  327. {edsl-0.1.46.dist-info → edsl-0.1.48.dist-info}/LICENSE +0 -0
  328. {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 edsl.scenarios.Scenario import Scenario
8
- from edsl.questions.QuestionBase import QuestionBase
9
- from edsl.questions.descriptors import QuestionOptionsDescriptor
10
- from edsl.questions.decorators import inject_exception
11
- from edsl.questions.response_validator_abc import ResponseValidatorABC
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
- """This question prompts the agent to select one option from a list of options.
116
-
117
- https://docs.expectedparrot.com/en/latest/questions.html#questionmultiplechoice-class
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
- """Instantiate a new QuestionMultipleChoice.
141
-
142
- :param question_name: The name of the question.
143
- :param question_text: The text of the question.
144
- :param question_options: The options the agent should select from.
145
- :param include_comment: Whether to include a comment field.
146
- :param use_code: Whether to use code for the options.
147
- :param answering_instructions: Instructions for the question.
148
- :param question_presentation: The presentation of the question.
149
- :param permissive: Whether to force the answer to be one of the options.
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 edsl.exceptions.questions import QuestionAnswerValidationError
9
- from edsl.questions.QuestionBase import QuestionBase
10
- from edsl.questions.descriptors import NumericalOrNoneDescriptor
11
- from edsl.questions.decorators import inject_exception
12
- from edsl.questions.response_validator_abc import ResponseValidatorABC
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 edsl.questions.QuestionBase import QuestionBase
7
- from edsl.questions.descriptors import (
6
+ from .question_base import QuestionBase
7
+ from .descriptors import (
8
8
  QuestionOptionsDescriptor,
9
9
  NumSelectionsDescriptor,
10
10
  )
11
- from edsl.questions.response_validator_abc import ResponseValidatorABC
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.QuestionRank import QuestionRank
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, uuid: Optional[Union[str, UUID]] = None, url: Optional[str] = None):
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(uuid, url, "question")
67
+ return coop.get(url_or_uuid, "question")
69
68
 
70
69
  @classmethod
71
- def delete(cls, uuid: Optional[Union[str, UUID]] = None, url: Optional[str] = None):
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(uuid, url)
75
+ return coop.delete(url_or_uuid)
77
76
 
78
77
  @classmethod
79
78
  def patch(
80
79
  cls,
81
- uuid: Optional[Union[str, UUID]] = None,
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(uuid, url, description, value, visibility)
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 edsl.results.Dataset import Dataset
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
- print(Question.available())
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 edsl.exceptions.questions import QuestionAnswerValidationError
7
- from edsl.questions.ExceptionExplainer import ExceptionExplainer
6
+ from .exceptions import QuestionAnswerValidationError
7
+ from .ExceptionExplainer import ExceptionExplainer
8
8
 
9
- from edsl.questions.data_structures import (
10
- RawEdslAnswerDict,
11
- EdslAnswerDict,
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.questions.QuestionAnswerValidationError:...
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.questions.QuestionAnswerValidationError:...
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
@@ -1,2 +1,5 @@
1
1
  # from edsl.results.Result import Result
2
- from edsl.results.Results import Results
2
+ from .results import Results
3
+ from .result import Result
4
+
5
+ __all__ = ["Results"]
@@ -1,5 +1,5 @@
1
- from edsl.exceptions.BaseException import BaseException
2
1
 
2
+ from ..base import BaseException
3
3
 
4
4
  class ResultsError(BaseException):
5
5
  relevant_docs = "https://docs.expectedparrot.com/en/latest/results.html"
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())