edsl 0.1.15__py3-none-any.whl → 0.1.40__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (407) hide show
  1. edsl/Base.py +348 -38
  2. edsl/BaseDiff.py +260 -0
  3. edsl/TemplateLoader.py +24 -0
  4. edsl/__init__.py +45 -10
  5. edsl/__version__.py +1 -1
  6. edsl/agents/Agent.py +842 -144
  7. edsl/agents/AgentList.py +521 -25
  8. edsl/agents/Invigilator.py +250 -374
  9. edsl/agents/InvigilatorBase.py +257 -0
  10. edsl/agents/PromptConstructor.py +272 -0
  11. edsl/agents/QuestionInstructionPromptBuilder.py +128 -0
  12. edsl/agents/QuestionTemplateReplacementsBuilder.py +137 -0
  13. edsl/agents/descriptors.py +43 -13
  14. edsl/agents/prompt_helpers.py +129 -0
  15. edsl/agents/question_option_processor.py +172 -0
  16. edsl/auto/AutoStudy.py +130 -0
  17. edsl/auto/StageBase.py +243 -0
  18. edsl/auto/StageGenerateSurvey.py +178 -0
  19. edsl/auto/StageLabelQuestions.py +125 -0
  20. edsl/auto/StagePersona.py +61 -0
  21. edsl/auto/StagePersonaDimensionValueRanges.py +88 -0
  22. edsl/auto/StagePersonaDimensionValues.py +74 -0
  23. edsl/auto/StagePersonaDimensions.py +69 -0
  24. edsl/auto/StageQuestions.py +74 -0
  25. edsl/auto/SurveyCreatorPipeline.py +21 -0
  26. edsl/auto/utilities.py +218 -0
  27. edsl/base/Base.py +279 -0
  28. edsl/config.py +115 -113
  29. edsl/conversation/Conversation.py +290 -0
  30. edsl/conversation/car_buying.py +59 -0
  31. edsl/conversation/chips.py +95 -0
  32. edsl/conversation/mug_negotiation.py +81 -0
  33. edsl/conversation/next_speaker_utilities.py +93 -0
  34. edsl/coop/CoopFunctionsMixin.py +15 -0
  35. edsl/coop/ExpectedParrotKeyHandler.py +125 -0
  36. edsl/coop/PriceFetcher.py +54 -0
  37. edsl/coop/__init__.py +1 -0
  38. edsl/coop/coop.py +1029 -134
  39. edsl/coop/utils.py +131 -0
  40. edsl/data/Cache.py +560 -89
  41. edsl/data/CacheEntry.py +230 -0
  42. edsl/data/CacheHandler.py +168 -0
  43. edsl/data/RemoteCacheSync.py +186 -0
  44. edsl/data/SQLiteDict.py +292 -0
  45. edsl/data/__init__.py +5 -3
  46. edsl/data/orm.py +6 -33
  47. edsl/data_transfer_models.py +74 -27
  48. edsl/enums.py +165 -8
  49. edsl/exceptions/BaseException.py +21 -0
  50. edsl/exceptions/__init__.py +52 -46
  51. edsl/exceptions/agents.py +33 -15
  52. edsl/exceptions/cache.py +5 -0
  53. edsl/exceptions/coop.py +8 -0
  54. edsl/exceptions/general.py +34 -0
  55. edsl/exceptions/inference_services.py +5 -0
  56. edsl/exceptions/jobs.py +15 -0
  57. edsl/exceptions/language_models.py +46 -1
  58. edsl/exceptions/questions.py +80 -5
  59. edsl/exceptions/results.py +16 -5
  60. edsl/exceptions/scenarios.py +29 -0
  61. edsl/exceptions/surveys.py +13 -10
  62. edsl/inference_services/AnthropicService.py +106 -0
  63. edsl/inference_services/AvailableModelCacheHandler.py +184 -0
  64. edsl/inference_services/AvailableModelFetcher.py +215 -0
  65. edsl/inference_services/AwsBedrock.py +118 -0
  66. edsl/inference_services/AzureAI.py +215 -0
  67. edsl/inference_services/DeepInfraService.py +18 -0
  68. edsl/inference_services/GoogleService.py +143 -0
  69. edsl/inference_services/GroqService.py +20 -0
  70. edsl/inference_services/InferenceServiceABC.py +80 -0
  71. edsl/inference_services/InferenceServicesCollection.py +138 -0
  72. edsl/inference_services/MistralAIService.py +120 -0
  73. edsl/inference_services/OllamaService.py +18 -0
  74. edsl/inference_services/OpenAIService.py +236 -0
  75. edsl/inference_services/PerplexityService.py +160 -0
  76. edsl/inference_services/ServiceAvailability.py +135 -0
  77. edsl/inference_services/TestService.py +90 -0
  78. edsl/inference_services/TogetherAIService.py +172 -0
  79. edsl/inference_services/data_structures.py +134 -0
  80. edsl/inference_services/models_available_cache.py +118 -0
  81. edsl/inference_services/rate_limits_cache.py +25 -0
  82. edsl/inference_services/registry.py +41 -0
  83. edsl/inference_services/write_available.py +10 -0
  84. edsl/jobs/AnswerQuestionFunctionConstructor.py +223 -0
  85. edsl/jobs/Answers.py +21 -20
  86. edsl/jobs/FetchInvigilator.py +47 -0
  87. edsl/jobs/InterviewTaskManager.py +98 -0
  88. edsl/jobs/InterviewsConstructor.py +50 -0
  89. edsl/jobs/Jobs.py +684 -206
  90. edsl/jobs/JobsChecks.py +172 -0
  91. edsl/jobs/JobsComponentConstructor.py +189 -0
  92. edsl/jobs/JobsPrompts.py +270 -0
  93. edsl/jobs/JobsRemoteInferenceHandler.py +311 -0
  94. edsl/jobs/JobsRemoteInferenceLogger.py +239 -0
  95. edsl/jobs/RequestTokenEstimator.py +30 -0
  96. edsl/jobs/async_interview_runner.py +138 -0
  97. edsl/jobs/buckets/BucketCollection.py +104 -0
  98. edsl/jobs/buckets/ModelBuckets.py +65 -0
  99. edsl/jobs/buckets/TokenBucket.py +283 -0
  100. edsl/jobs/buckets/TokenBucketAPI.py +211 -0
  101. edsl/jobs/buckets/TokenBucketClient.py +191 -0
  102. edsl/jobs/check_survey_scenario_compatibility.py +85 -0
  103. edsl/jobs/data_structures.py +120 -0
  104. edsl/jobs/decorators.py +35 -0
  105. edsl/jobs/interviews/Interview.py +392 -0
  106. edsl/jobs/interviews/InterviewExceptionCollection.py +99 -0
  107. edsl/jobs/interviews/InterviewExceptionEntry.py +186 -0
  108. edsl/jobs/interviews/InterviewStatistic.py +63 -0
  109. edsl/jobs/interviews/InterviewStatisticsCollection.py +25 -0
  110. edsl/jobs/interviews/InterviewStatusDictionary.py +78 -0
  111. edsl/jobs/interviews/InterviewStatusLog.py +92 -0
  112. edsl/jobs/interviews/ReportErrors.py +66 -0
  113. edsl/jobs/interviews/interview_status_enum.py +9 -0
  114. edsl/jobs/jobs_status_enums.py +9 -0
  115. edsl/jobs/loggers/HTMLTableJobLogger.py +304 -0
  116. edsl/jobs/results_exceptions_handler.py +98 -0
  117. edsl/jobs/runners/JobsRunnerAsyncio.py +151 -110
  118. edsl/jobs/runners/JobsRunnerStatus.py +298 -0
  119. edsl/jobs/tasks/QuestionTaskCreator.py +244 -0
  120. edsl/jobs/tasks/TaskCreators.py +64 -0
  121. edsl/jobs/tasks/TaskHistory.py +470 -0
  122. edsl/jobs/tasks/TaskStatusLog.py +23 -0
  123. edsl/jobs/tasks/task_status_enum.py +161 -0
  124. edsl/jobs/tokens/InterviewTokenUsage.py +27 -0
  125. edsl/jobs/tokens/TokenUsage.py +34 -0
  126. edsl/language_models/ComputeCost.py +63 -0
  127. edsl/language_models/LanguageModel.py +507 -386
  128. edsl/language_models/ModelList.py +164 -0
  129. edsl/language_models/PriceManager.py +127 -0
  130. edsl/language_models/RawResponseHandler.py +106 -0
  131. edsl/language_models/RegisterLanguageModelsMeta.py +184 -0
  132. edsl/language_models/__init__.py +1 -8
  133. edsl/language_models/fake_openai_call.py +15 -0
  134. edsl/language_models/fake_openai_service.py +61 -0
  135. edsl/language_models/key_management/KeyLookup.py +63 -0
  136. edsl/language_models/key_management/KeyLookupBuilder.py +273 -0
  137. edsl/language_models/key_management/KeyLookupCollection.py +38 -0
  138. edsl/language_models/key_management/__init__.py +0 -0
  139. edsl/language_models/key_management/models.py +131 -0
  140. edsl/language_models/model.py +256 -0
  141. edsl/language_models/repair.py +109 -41
  142. edsl/language_models/utilities.py +65 -0
  143. edsl/notebooks/Notebook.py +263 -0
  144. edsl/notebooks/NotebookToLaTeX.py +142 -0
  145. edsl/notebooks/__init__.py +1 -0
  146. edsl/prompts/Prompt.py +222 -93
  147. edsl/prompts/__init__.py +1 -1
  148. edsl/questions/ExceptionExplainer.py +77 -0
  149. edsl/questions/HTMLQuestion.py +103 -0
  150. edsl/questions/QuestionBase.py +518 -0
  151. edsl/questions/QuestionBasePromptsMixin.py +221 -0
  152. edsl/questions/QuestionBudget.py +164 -67
  153. edsl/questions/QuestionCheckBox.py +281 -62
  154. edsl/questions/QuestionDict.py +343 -0
  155. edsl/questions/QuestionExtract.py +136 -50
  156. edsl/questions/QuestionFreeText.py +79 -55
  157. edsl/questions/QuestionFunctional.py +138 -41
  158. edsl/questions/QuestionList.py +184 -57
  159. edsl/questions/QuestionMatrix.py +265 -0
  160. edsl/questions/QuestionMultipleChoice.py +293 -69
  161. edsl/questions/QuestionNumerical.py +109 -56
  162. edsl/questions/QuestionRank.py +244 -49
  163. edsl/questions/Quick.py +41 -0
  164. edsl/questions/SimpleAskMixin.py +74 -0
  165. edsl/questions/__init__.py +9 -6
  166. edsl/questions/{AnswerValidatorMixin.py → answer_validator_mixin.py} +153 -38
  167. edsl/questions/compose_questions.py +13 -7
  168. edsl/questions/data_structures.py +20 -0
  169. edsl/questions/decorators.py +21 -0
  170. edsl/questions/derived/QuestionLikertFive.py +28 -26
  171. edsl/questions/derived/QuestionLinearScale.py +41 -28
  172. edsl/questions/derived/QuestionTopK.py +34 -26
  173. edsl/questions/derived/QuestionYesNo.py +40 -27
  174. edsl/questions/descriptors.py +228 -74
  175. edsl/questions/loop_processor.py +149 -0
  176. edsl/questions/prompt_templates/question_budget.jinja +13 -0
  177. edsl/questions/prompt_templates/question_checkbox.jinja +32 -0
  178. edsl/questions/prompt_templates/question_extract.jinja +11 -0
  179. edsl/questions/prompt_templates/question_free_text.jinja +3 -0
  180. edsl/questions/prompt_templates/question_linear_scale.jinja +11 -0
  181. edsl/questions/prompt_templates/question_list.jinja +17 -0
  182. edsl/questions/prompt_templates/question_multiple_choice.jinja +33 -0
  183. edsl/questions/prompt_templates/question_numerical.jinja +37 -0
  184. edsl/questions/question_base_gen_mixin.py +168 -0
  185. edsl/questions/question_registry.py +130 -46
  186. edsl/questions/register_questions_meta.py +71 -0
  187. edsl/questions/response_validator_abc.py +188 -0
  188. edsl/questions/response_validator_factory.py +34 -0
  189. edsl/questions/settings.py +5 -2
  190. edsl/questions/templates/__init__.py +0 -0
  191. edsl/questions/templates/budget/__init__.py +0 -0
  192. edsl/questions/templates/budget/answering_instructions.jinja +7 -0
  193. edsl/questions/templates/budget/question_presentation.jinja +7 -0
  194. edsl/questions/templates/checkbox/__init__.py +0 -0
  195. edsl/questions/templates/checkbox/answering_instructions.jinja +10 -0
  196. edsl/questions/templates/checkbox/question_presentation.jinja +22 -0
  197. edsl/questions/templates/dict/__init__.py +0 -0
  198. edsl/questions/templates/dict/answering_instructions.jinja +21 -0
  199. edsl/questions/templates/dict/question_presentation.jinja +1 -0
  200. edsl/questions/templates/extract/__init__.py +0 -0
  201. edsl/questions/templates/extract/answering_instructions.jinja +7 -0
  202. edsl/questions/templates/extract/question_presentation.jinja +1 -0
  203. edsl/questions/templates/free_text/__init__.py +0 -0
  204. edsl/questions/templates/free_text/answering_instructions.jinja +0 -0
  205. edsl/questions/templates/free_text/question_presentation.jinja +1 -0
  206. edsl/questions/templates/likert_five/__init__.py +0 -0
  207. edsl/questions/templates/likert_five/answering_instructions.jinja +10 -0
  208. edsl/questions/templates/likert_five/question_presentation.jinja +12 -0
  209. edsl/questions/templates/linear_scale/__init__.py +0 -0
  210. edsl/questions/templates/linear_scale/answering_instructions.jinja +5 -0
  211. edsl/questions/templates/linear_scale/question_presentation.jinja +5 -0
  212. edsl/questions/templates/list/__init__.py +0 -0
  213. edsl/questions/templates/list/answering_instructions.jinja +4 -0
  214. edsl/questions/templates/list/question_presentation.jinja +5 -0
  215. edsl/questions/templates/matrix/__init__.py +1 -0
  216. edsl/questions/templates/matrix/answering_instructions.jinja +5 -0
  217. edsl/questions/templates/matrix/question_presentation.jinja +20 -0
  218. edsl/questions/templates/multiple_choice/__init__.py +0 -0
  219. edsl/questions/templates/multiple_choice/answering_instructions.jinja +9 -0
  220. edsl/questions/templates/multiple_choice/html.jinja +0 -0
  221. edsl/questions/templates/multiple_choice/question_presentation.jinja +12 -0
  222. edsl/questions/templates/numerical/__init__.py +0 -0
  223. edsl/questions/templates/numerical/answering_instructions.jinja +7 -0
  224. edsl/questions/templates/numerical/question_presentation.jinja +7 -0
  225. edsl/questions/templates/rank/__init__.py +0 -0
  226. edsl/questions/templates/rank/answering_instructions.jinja +11 -0
  227. edsl/questions/templates/rank/question_presentation.jinja +15 -0
  228. edsl/questions/templates/top_k/__init__.py +0 -0
  229. edsl/questions/templates/top_k/answering_instructions.jinja +8 -0
  230. edsl/questions/templates/top_k/question_presentation.jinja +22 -0
  231. edsl/questions/templates/yes_no/__init__.py +0 -0
  232. edsl/questions/templates/yes_no/answering_instructions.jinja +6 -0
  233. edsl/questions/templates/yes_no/question_presentation.jinja +12 -0
  234. edsl/results/CSSParameterizer.py +108 -0
  235. edsl/results/Dataset.py +550 -19
  236. edsl/results/DatasetExportMixin.py +594 -0
  237. edsl/results/DatasetTree.py +295 -0
  238. edsl/results/MarkdownToDocx.py +122 -0
  239. edsl/results/MarkdownToPDF.py +111 -0
  240. edsl/results/Result.py +477 -173
  241. edsl/results/Results.py +987 -269
  242. edsl/results/ResultsExportMixin.py +28 -125
  243. edsl/results/ResultsGGMixin.py +83 -15
  244. edsl/results/TableDisplay.py +125 -0
  245. edsl/results/TextEditor.py +50 -0
  246. edsl/results/__init__.py +1 -1
  247. edsl/results/file_exports.py +252 -0
  248. edsl/results/results_fetch_mixin.py +33 -0
  249. edsl/results/results_selector.py +145 -0
  250. edsl/results/results_tools_mixin.py +98 -0
  251. edsl/results/smart_objects.py +96 -0
  252. edsl/results/table_data_class.py +12 -0
  253. edsl/results/table_display.css +78 -0
  254. edsl/results/table_renderers.py +118 -0
  255. edsl/results/tree_explore.py +115 -0
  256. edsl/scenarios/ConstructDownloadLink.py +109 -0
  257. edsl/scenarios/DocumentChunker.py +102 -0
  258. edsl/scenarios/DocxScenario.py +16 -0
  259. edsl/scenarios/FileStore.py +543 -0
  260. edsl/scenarios/PdfExtractor.py +40 -0
  261. edsl/scenarios/Scenario.py +431 -62
  262. edsl/scenarios/ScenarioHtmlMixin.py +65 -0
  263. edsl/scenarios/ScenarioList.py +1415 -45
  264. edsl/scenarios/ScenarioListExportMixin.py +45 -0
  265. edsl/scenarios/ScenarioListPdfMixin.py +239 -0
  266. edsl/scenarios/__init__.py +2 -0
  267. edsl/scenarios/directory_scanner.py +96 -0
  268. edsl/scenarios/file_methods.py +85 -0
  269. edsl/scenarios/handlers/__init__.py +13 -0
  270. edsl/scenarios/handlers/csv.py +49 -0
  271. edsl/scenarios/handlers/docx.py +76 -0
  272. edsl/scenarios/handlers/html.py +37 -0
  273. edsl/scenarios/handlers/json.py +111 -0
  274. edsl/scenarios/handlers/latex.py +5 -0
  275. edsl/scenarios/handlers/md.py +51 -0
  276. edsl/scenarios/handlers/pdf.py +68 -0
  277. edsl/scenarios/handlers/png.py +39 -0
  278. edsl/scenarios/handlers/pptx.py +105 -0
  279. edsl/scenarios/handlers/py.py +294 -0
  280. edsl/scenarios/handlers/sql.py +313 -0
  281. edsl/scenarios/handlers/sqlite.py +149 -0
  282. edsl/scenarios/handlers/txt.py +33 -0
  283. edsl/scenarios/scenario_join.py +131 -0
  284. edsl/scenarios/scenario_selector.py +156 -0
  285. edsl/shared.py +1 -0
  286. edsl/study/ObjectEntry.py +173 -0
  287. edsl/study/ProofOfWork.py +113 -0
  288. edsl/study/SnapShot.py +80 -0
  289. edsl/study/Study.py +521 -0
  290. edsl/study/__init__.py +4 -0
  291. edsl/surveys/ConstructDAG.py +92 -0
  292. edsl/surveys/DAG.py +92 -11
  293. edsl/surveys/EditSurvey.py +221 -0
  294. edsl/surveys/InstructionHandler.py +100 -0
  295. edsl/surveys/Memory.py +9 -4
  296. edsl/surveys/MemoryManagement.py +72 -0
  297. edsl/surveys/MemoryPlan.py +156 -35
  298. edsl/surveys/Rule.py +221 -74
  299. edsl/surveys/RuleCollection.py +241 -61
  300. edsl/surveys/RuleManager.py +172 -0
  301. edsl/surveys/Simulator.py +75 -0
  302. edsl/surveys/Survey.py +1079 -339
  303. edsl/surveys/SurveyCSS.py +273 -0
  304. edsl/surveys/SurveyExportMixin.py +235 -40
  305. edsl/surveys/SurveyFlowVisualization.py +181 -0
  306. edsl/surveys/SurveyQualtricsImport.py +284 -0
  307. edsl/surveys/SurveyToApp.py +141 -0
  308. edsl/surveys/__init__.py +4 -2
  309. edsl/surveys/base.py +19 -3
  310. edsl/surveys/descriptors.py +17 -6
  311. edsl/surveys/instructions/ChangeInstruction.py +48 -0
  312. edsl/surveys/instructions/Instruction.py +56 -0
  313. edsl/surveys/instructions/InstructionCollection.py +82 -0
  314. edsl/surveys/instructions/__init__.py +0 -0
  315. edsl/templates/error_reporting/base.html +24 -0
  316. edsl/templates/error_reporting/exceptions_by_model.html +35 -0
  317. edsl/templates/error_reporting/exceptions_by_question_name.html +17 -0
  318. edsl/templates/error_reporting/exceptions_by_type.html +17 -0
  319. edsl/templates/error_reporting/interview_details.html +116 -0
  320. edsl/templates/error_reporting/interviews.html +19 -0
  321. edsl/templates/error_reporting/overview.html +5 -0
  322. edsl/templates/error_reporting/performance_plot.html +2 -0
  323. edsl/templates/error_reporting/report.css +74 -0
  324. edsl/templates/error_reporting/report.html +118 -0
  325. edsl/templates/error_reporting/report.js +25 -0
  326. edsl/tools/__init__.py +1 -0
  327. edsl/tools/clusters.py +192 -0
  328. edsl/tools/embeddings.py +27 -0
  329. edsl/tools/embeddings_plotting.py +118 -0
  330. edsl/tools/plotting.py +112 -0
  331. edsl/tools/summarize.py +18 -0
  332. edsl/utilities/PrettyList.py +56 -0
  333. edsl/utilities/SystemInfo.py +5 -0
  334. edsl/utilities/__init__.py +21 -20
  335. edsl/utilities/ast_utilities.py +3 -0
  336. edsl/utilities/data/Registry.py +2 -0
  337. edsl/utilities/decorators.py +41 -0
  338. edsl/utilities/gcp_bucket/__init__.py +0 -0
  339. edsl/utilities/gcp_bucket/cloud_storage.py +96 -0
  340. edsl/utilities/interface.py +310 -60
  341. edsl/utilities/is_notebook.py +18 -0
  342. edsl/utilities/is_valid_variable_name.py +11 -0
  343. edsl/utilities/naming_utilities.py +263 -0
  344. edsl/utilities/remove_edsl_version.py +24 -0
  345. edsl/utilities/repair_functions.py +28 -0
  346. edsl/utilities/restricted_python.py +70 -0
  347. edsl/utilities/utilities.py +203 -13
  348. edsl-0.1.40.dist-info/METADATA +111 -0
  349. edsl-0.1.40.dist-info/RECORD +362 -0
  350. {edsl-0.1.15.dist-info → edsl-0.1.40.dist-info}/WHEEL +1 -1
  351. edsl/agents/AgentListExportMixin.py +0 -24
  352. edsl/coop/old.py +0 -31
  353. edsl/data/Database.py +0 -141
  354. edsl/data/crud.py +0 -121
  355. edsl/jobs/Interview.py +0 -435
  356. edsl/jobs/JobsRunner.py +0 -63
  357. edsl/jobs/JobsRunnerStatusMixin.py +0 -115
  358. edsl/jobs/base.py +0 -47
  359. edsl/jobs/buckets.py +0 -178
  360. edsl/jobs/runners/JobsRunnerDryRun.py +0 -19
  361. edsl/jobs/runners/JobsRunnerStreaming.py +0 -54
  362. edsl/jobs/task_management.py +0 -215
  363. edsl/jobs/token_tracking.py +0 -78
  364. edsl/language_models/DeepInfra.py +0 -69
  365. edsl/language_models/OpenAI.py +0 -98
  366. edsl/language_models/model_interfaces/GeminiPro.py +0 -66
  367. edsl/language_models/model_interfaces/LanguageModelOpenAIFour.py +0 -8
  368. edsl/language_models/model_interfaces/LanguageModelOpenAIThreeFiveTurbo.py +0 -8
  369. edsl/language_models/model_interfaces/LlamaTwo13B.py +0 -21
  370. edsl/language_models/model_interfaces/LlamaTwo70B.py +0 -21
  371. edsl/language_models/model_interfaces/Mixtral8x7B.py +0 -24
  372. edsl/language_models/registry.py +0 -81
  373. edsl/language_models/schemas.py +0 -15
  374. edsl/language_models/unused/ReplicateBase.py +0 -83
  375. edsl/prompts/QuestionInstructionsBase.py +0 -6
  376. edsl/prompts/library/agent_instructions.py +0 -29
  377. edsl/prompts/library/agent_persona.py +0 -17
  378. edsl/prompts/library/question_budget.py +0 -26
  379. edsl/prompts/library/question_checkbox.py +0 -32
  380. edsl/prompts/library/question_extract.py +0 -19
  381. edsl/prompts/library/question_freetext.py +0 -14
  382. edsl/prompts/library/question_linear_scale.py +0 -20
  383. edsl/prompts/library/question_list.py +0 -22
  384. edsl/prompts/library/question_multiple_choice.py +0 -44
  385. edsl/prompts/library/question_numerical.py +0 -31
  386. edsl/prompts/library/question_rank.py +0 -21
  387. edsl/prompts/prompt_config.py +0 -33
  388. edsl/prompts/registry.py +0 -185
  389. edsl/questions/Question.py +0 -240
  390. edsl/report/InputOutputDataTypes.py +0 -134
  391. edsl/report/RegressionMixin.py +0 -28
  392. edsl/report/ReportOutputs.py +0 -1228
  393. edsl/report/ResultsFetchMixin.py +0 -106
  394. edsl/report/ResultsOutputMixin.py +0 -14
  395. edsl/report/demo.ipynb +0 -645
  396. edsl/results/ResultsDBMixin.py +0 -184
  397. edsl/surveys/SurveyFlowVisualizationMixin.py +0 -92
  398. edsl/trackers/Tracker.py +0 -91
  399. edsl/trackers/TrackerAPI.py +0 -196
  400. edsl/trackers/TrackerTasks.py +0 -70
  401. edsl/utilities/pastebin.py +0 -141
  402. edsl-0.1.15.dist-info/METADATA +0 -69
  403. edsl-0.1.15.dist-info/RECORD +0 -142
  404. /edsl/{language_models/model_interfaces → inference_services}/__init__.py +0 -0
  405. /edsl/{report/__init__.py → jobs/runners/JobsRunnerStatusData.py} +0 -0
  406. /edsl/{trackers/__init__.py → language_models/ServiceDataSources.py} +0 -0
  407. {edsl-0.1.15.dist-info → edsl-0.1.40.dist-info}/LICENSE +0 -0
edsl/results/Results.py CHANGED
@@ -1,179 +1,542 @@
1
+ """
2
+ The Results object is the result of running a survey.
3
+ It is not typically instantiated directly, but is returned by the run method of a `Job` object.
4
+ """
5
+
1
6
  from __future__ import annotations
2
7
  import json
3
- import io
8
+ import random
4
9
  from collections import UserList, defaultdict
10
+ from typing import Optional, Callable, Any, Type, Union, List, TYPE_CHECKING
5
11
 
6
- from rich.console import Console
12
+ from bisect import bisect_left
7
13
 
8
- from simpleeval import EvalWithCompoundTypes
9
- from typing import Any, Type, Union
10
- from edsl.exceptions import (
14
+ from edsl.Base import Base
15
+ from edsl.exceptions.results import (
16
+ ResultsError,
11
17
  ResultsBadMutationstringError,
12
18
  ResultsColumnNotFoundError,
13
19
  ResultsInvalidNameError,
14
20
  ResultsMutateError,
21
+ ResultsFilterError,
22
+ ResultsDeserializationError,
15
23
  )
16
- from edsl.agents import Agent
17
- from edsl.data import CRUD
18
- from edsl.language_models import LanguageModel
19
- from edsl.results.Dataset import Dataset
20
- from edsl.results.Result import Result
21
- from edsl.results.ResultsExportMixin import ResultsExportMixin
22
- from edsl.scenarios import Scenario
23
- from edsl.surveys import Survey
24
- from edsl.utilities import (
25
- is_gzipped,
26
- is_valid_variable_name,
27
- print_public_methods_with_doc,
28
- shorten_string,
29
- is_notebook,
30
- )
31
-
32
- from edsl.results.ResultsDBMixin import ResultsDBMixin
33
-
34
-
35
- class Mixins(ResultsExportMixin, ResultsDBMixin):
36
- pass
37
-
38
-
39
- # These are only made available if the user has installed
40
- # our package from pip with the [report] option
41
- try:
42
- from edsl.report.RegressionMixin import RegressionMixin
43
-
44
- Mixins = type("Mixins", (RegressionMixin, Mixins), {})
45
- except (ImportError, ModuleNotFoundError):
46
- pass
47
24
 
48
- try:
49
- from edsl.report.ResultsFetchMixin import ResultsFetchMixin
25
+ if TYPE_CHECKING:
26
+ from edsl.surveys.Survey import Survey
27
+ from edsl.data.Cache import Cache
28
+ from edsl.agents.AgentList import AgentList
29
+ from edsl.language_models.model import Model
30
+ from edsl.scenarios.ScenarioList import ScenarioList
31
+ from edsl.results.Result import Result
32
+ from edsl.jobs.tasks.TaskHistory import TaskHistory
33
+ from edsl.language_models.ModelList import ModelList
34
+ from simpleeval import EvalWithCompoundTypes
50
35
 
51
- Mixins = type("Mixins", (ResultsFetchMixin, Mixins), {})
52
- except (ImportError, ModuleNotFoundError):
53
- pass
54
-
55
- try:
56
- from edsl.report.ResultsOutputMixin import ResultsOutputMixin
57
-
58
- Mixins = type("Mixins", (ResultsOutputMixin, Mixins), {})
59
- except (ImportError, ModuleNotFoundError):
60
- pass
36
+ from edsl.results.ResultsExportMixin import ResultsExportMixin
37
+ from edsl.results.ResultsGGMixin import ResultsGGMixin
38
+ from edsl.results.results_fetch_mixin import ResultsFetchMixin
39
+ from edsl.utilities.remove_edsl_version import remove_edsl_version
40
+
41
+
42
+ class Mixins(
43
+ ResultsExportMixin,
44
+ ResultsFetchMixin,
45
+ ResultsGGMixin,
46
+ ):
47
+ def long(self):
48
+ return self.table().long()
49
+
50
+ def print_long(self, max_rows: int = None) -> None:
51
+ """Print the results in long format.
52
+
53
+ >>> from edsl.results import Results
54
+ >>> r = Results.example()
55
+ >>> r.select('how_feeling').print_long(max_rows = 2)
56
+ ┏━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━┓
57
+ ┃ Result index ┃ Key ┃ Value ┃
58
+ ┡━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━┩
59
+ │ 0 │ how_feeling │ OK │
60
+ │ 1 │ how_feeling │ Great │
61
+ └──────────────┴─────────────┴───────┘
62
+ """
63
+ from edsl.utilities.interface import print_results_long
61
64
 
62
- from edsl.Base import Base
65
+ print_results_long(self, max_rows=max_rows)
63
66
 
64
67
 
65
68
  class Results(UserList, Mixins, Base):
66
69
  """
67
70
  This class is a UserList of Result objects.
68
- - It is instantiated with a Survey and a list of Result objects (observations).
69
- - It can be manipulated in various ways with select, filter, mutate, etc.
70
- - It also has a list of created_columns, which is a list of columns that have been created with `mutate`
71
+
72
+ It is instantiated with a `Survey` and a list of `Result` objects.
73
+ It can be manipulated in various ways with select, filter, mutate, etc.
74
+ It also has a list of created_columns, which are columns that have been created with `mutate` and are not part of the original data.
71
75
  """
72
76
 
77
+ __documentation__ = "https://docs.expectedparrot.com/en/latest/results.html"
78
+
79
+ known_data_types = [
80
+ "answer",
81
+ "scenario",
82
+ "agent",
83
+ "model",
84
+ "prompt",
85
+ "raw_model_response",
86
+ "iteration",
87
+ "question_text",
88
+ "question_options",
89
+ "question_type",
90
+ "comment",
91
+ "generated_tokens",
92
+ "cache_used",
93
+ "cache_keys",
94
+ ]
95
+
73
96
  def __init__(
74
97
  self,
75
- survey: Survey = None,
76
- data: list[Result] = None,
77
- created_columns: list = None,
78
- job_uuid: str = None,
79
- total_results: int = None,
98
+ survey: Optional[Survey] = None,
99
+ data: Optional[list[Result]] = None,
100
+ created_columns: Optional[list[str]] = None,
101
+ cache: Optional[Cache] = None,
102
+ job_uuid: Optional[str] = None,
103
+ total_results: Optional[int] = None,
104
+ task_history: Optional[TaskHistory] = None,
80
105
  ):
106
+ """Instantiate a `Results` object with a survey and a list of `Result` objects.
107
+
108
+ :param survey: A Survey object.
109
+ :param data: A list of Result objects.
110
+ :param created_columns: A list of strings that are created columns.
111
+ :param job_uuid: A string representing the job UUID.
112
+ :param total_results: An integer representing the total number of results.
113
+ :cache: A Cache object.
114
+ """
81
115
  super().__init__(data)
116
+ from edsl.data.Cache import Cache
117
+ from edsl.jobs.tasks.TaskHistory import TaskHistory
118
+
82
119
  self.survey = survey
83
120
  self.created_columns = created_columns or []
84
121
  self._job_uuid = job_uuid
85
122
  self._total_results = total_results
123
+ self.cache = cache or Cache()
86
124
 
87
- if hasattr(self, "add_output_functions"):
88
- self.add_output_functions()
125
+ self.task_history = task_history or TaskHistory(interviews=[])
89
126
 
90
- ######################
91
- # Streaming methods
92
- ######################
127
+ if hasattr(self, "_add_output_functions"):
128
+ self._add_output_functions()
129
+
130
+ def _summary(self) -> dict:
131
+ import reprlib
132
+
133
+ d = {
134
+ "observations": len(self),
135
+ "agents": len(set(self.agents)),
136
+ "models": len(set(self.models)),
137
+ "scenarios": len(set(self.scenarios)),
138
+ "questions": len(self.survey),
139
+ "Survey question names": reprlib.repr(self.survey.question_names),
140
+ }
141
+ return d
142
+
143
+ def _cache_keys(self):
144
+ cache_keys = []
145
+ for result in self:
146
+ cache_keys.extend(list(result["cache_keys"].values()))
147
+ return cache_keys
148
+
149
+ def relevant_cache(self, cache: Cache) -> Cache:
150
+ cache_keys = self._cache_keys()
151
+ return cache.subset(cache_keys)
152
+
153
+ def insert(self, item):
154
+ item_order = getattr(item, "order", None)
155
+ if item_order is not None:
156
+ # Get list of orders, putting None at the end
157
+ orders = [getattr(x, "order", None) for x in self]
158
+ # Filter to just the non-None orders for bisect
159
+ sorted_orders = [x for x in orders if x is not None]
160
+ if sorted_orders:
161
+ index = bisect_left(sorted_orders, item_order)
162
+ # Account for any None values before this position
163
+ index += orders[:index].count(None)
164
+ else:
165
+ # If no sorted items yet, insert before any unordered items
166
+ index = 0
167
+ self.data.insert(index, item)
168
+ else:
169
+ # No order - append to end
170
+ self.data.append(item)
171
+
172
+ def append(self, item):
173
+ self.insert(item)
174
+
175
+ def extend(self, other):
176
+ for item in other:
177
+ self.insert(item)
178
+
179
+ def compute_job_cost(self, include_cached_responses_in_cost: bool = False) -> float:
180
+ """
181
+ Computes the cost of a completed job in USD.
182
+ """
183
+ total_cost = 0
184
+ for result in self:
185
+ for key in result["raw_model_response"]:
186
+ if key.endswith("_cost"):
187
+ result_cost = result["raw_model_response"][key]
188
+
189
+ question_name = key.removesuffix("_cost")
190
+ cache_used = result["cache_used_dict"][question_name]
191
+
192
+ if isinstance(result_cost, (int, float)):
193
+ if include_cached_responses_in_cost:
194
+ total_cost += result_cost
195
+ elif not include_cached_responses_in_cost and not cache_used:
196
+ total_cost += result_cost
197
+
198
+ return total_cost
199
+
200
+ def leaves(self):
201
+ leaves = []
202
+ for result in self:
203
+ leaves.extend(result.leaves())
204
+ return leaves
205
+
206
+ def tree(self, node_list: Optional[List[str]] = None):
207
+ return self.to_scenario_list().tree(node_list)
208
+
209
+ def interactive_tree(
210
+ self,
211
+ fold_attributes: Optional[List[str]] = None,
212
+ drop: Optional[List[str]] = None,
213
+ open_file=True,
214
+ ) -> dict:
215
+ """Return the results as a tree."""
216
+ from edsl.results.tree_explore import FoldableHTMLTableGenerator
217
+
218
+ if drop is None:
219
+ drop = []
220
+
221
+ valid_attributes = [
222
+ "model",
223
+ "scenario",
224
+ "agent",
225
+ "answer",
226
+ "question",
227
+ "iteration",
228
+ ]
229
+ if fold_attributes is None:
230
+ fold_attributes = []
231
+
232
+ for attribute in fold_attributes:
233
+ if attribute not in valid_attributes:
234
+ raise ValueError(
235
+ f"Invalid fold attribute: {attribute}; must be in {valid_attributes}"
236
+ )
237
+ data = self.leaves()
238
+ generator = FoldableHTMLTableGenerator(data)
239
+ tree = generator.tree(fold_attributes=fold_attributes, drop=drop)
240
+ html_content = generator.generate_html(tree, fold_attributes)
241
+ import tempfile
242
+ from edsl.utilities.utilities import is_notebook
243
+
244
+ from IPython.display import display, HTML
245
+
246
+ if is_notebook():
247
+ import html
248
+ from IPython.display import display, HTML
249
+
250
+ height = 1000
251
+ width = 1000
252
+ escaped_output = html.escape(html_content)
253
+ # escaped_output = rendered_html
254
+ iframe = f""""
255
+ <iframe srcdoc="{ escaped_output }" style="width: {width}px; height: {height}px;"></iframe>
256
+ """
257
+ display(HTML(iframe))
258
+ return None
259
+
260
+ with tempfile.NamedTemporaryFile(suffix=".html", delete=False) as f:
261
+ f.write(html_content.encode())
262
+ print(f"HTML file has been generated: {f.name}")
263
+
264
+ if open_file:
265
+ import webbrowser
266
+ import time
267
+
268
+ time.sleep(1) # Wait for 1 second
269
+ # webbrowser.open(f.name)
270
+ import os
271
+
272
+ filename = f.name
273
+ webbrowser.open(f"file://{os.path.abspath(filename)}")
274
+
275
+ else:
276
+ return html_content
93
277
 
94
278
  def code(self):
95
279
  raise NotImplementedError
96
280
 
97
281
  def __getitem__(self, i):
282
+ if isinstance(i, int):
283
+ return self.data[i]
284
+
98
285
  if isinstance(i, slice):
99
- # Return a sliced view of the list
100
286
  return self.__class__(survey=self.survey, data=self.data[i])
101
- else:
102
- # Return a single item
103
- return self.data[i]
104
287
 
105
- def _update_results(self) -> None:
106
- if self._job_uuid and len(self.data) < self._total_results:
107
- results = [
108
- Result(
109
- agent=Agent.from_dict(json.loads(r.agent)),
110
- scenario=Scenario.from_dict(json.loads(r.scenario)),
111
- model=LanguageModel.from_dict(json.loads(r.model)),
112
- iteration=1,
113
- answer=json.loads(r.answer),
114
- )
115
- for r in CRUD.read_results(self._job_uuid)
116
- ]
117
- self.data = results
288
+ if isinstance(i, str):
289
+ return self.to_dict()[i]
290
+
291
+ raise TypeError("Invalid argument type")
292
+
293
+ def __add__(self, other: Results) -> Results:
294
+ """Add two Results objects together.
295
+ They must have the same survey and created columns.
296
+ :param other: A Results object.
297
+
298
+ Example:
299
+
300
+ >>> r = Results.example()
301
+ >>> r2 = Results.example()
302
+ >>> r3 = r + r2
303
+ """
304
+ if self.survey != other.survey:
305
+ raise ResultsError(
306
+ "The surveys are not the same so the the results cannot be added together."
307
+ )
308
+ if self.created_columns != other.created_columns:
309
+ raise ResultsError(
310
+ "The created columns are not the same so they cannot be added together."
311
+ )
312
+
313
+ return Results(
314
+ survey=self.survey,
315
+ data=self.data + other.data,
316
+ created_columns=self.created_columns,
317
+ )
118
318
 
119
319
  def __repr__(self) -> str:
120
- self._update_results()
121
- if self._job_uuid and len(self.data) < self._total_results:
122
- print(f"Completeness: {len(self.data)}/{self._total_results}")
123
- data = [repr(result) for result in self.data]
124
- if is_notebook():
125
- return self.rich_print()
320
+ return f"Results(data = {self.data}, survey = {repr(self.survey)}, created_columns = {self.created_columns})"
321
+
322
+ def table(
323
+ self,
324
+ *fields,
325
+ tablefmt: Optional[str] = None,
326
+ pretty_labels: Optional[dict] = None,
327
+ print_parameters: Optional[dict] = None,
328
+ ):
329
+ new_fields = []
330
+ for field in fields:
331
+ if "." in field:
332
+ data_type, key = field.split(".")
333
+ if data_type not in self.known_data_types:
334
+ raise ResultsInvalidNameError(
335
+ f"{data_type} is not a valid data type. Must be in {self.known_data_types}"
336
+ )
337
+ if key == "*":
338
+ for k in self._data_type_to_keys[data_type]:
339
+ new_fields.append(k)
340
+ else:
341
+ if key not in self._key_to_data_type:
342
+ raise ResultsColumnNotFoundError(
343
+ f"{key} is not a valid key. Must be in {self._key_to_data_type}"
344
+ )
345
+ new_fields.append(key)
346
+ else:
347
+ new_fields.append(field)
348
+
349
+ return (
350
+ self.to_scenario_list()
351
+ .to_dataset()
352
+ .table(
353
+ *new_fields,
354
+ tablefmt=tablefmt,
355
+ pretty_labels=pretty_labels,
356
+ print_parameters=print_parameters,
357
+ )
358
+ )
359
+
360
+ def to_dict(
361
+ self,
362
+ sort: bool = False,
363
+ add_edsl_version: bool = False,
364
+ include_cache: bool = True,
365
+ include_task_history: bool = False,
366
+ include_cache_info: bool = True,
367
+ ) -> dict[str, Any]:
368
+ from edsl.data.Cache import Cache
369
+
370
+ if sort:
371
+ data = sorted([result for result in self.data], key=lambda x: hash(x))
126
372
  else:
127
- return f"Results(data = {data}, survey = {repr(self.survey)}, created_columns = {self.created_columns})"
373
+ data = [result for result in self.data]
128
374
 
129
- def to_dict(self) -> dict[str, Any]:
130
- """Converts the Results object to a dictionary"""
131
- return {
132
- "data": [result.to_dict() for result in self.data],
133
- "survey": self.survey.to_dict(),
375
+ d = {
376
+ "data": [
377
+ result.to_dict(
378
+ add_edsl_version=add_edsl_version,
379
+ include_cache_info=include_cache_info,
380
+ )
381
+ for result in data
382
+ ],
383
+ "survey": self.survey.to_dict(add_edsl_version=add_edsl_version),
134
384
  "created_columns": self.created_columns,
135
385
  }
386
+ if include_cache:
387
+ d.update(
388
+ {
389
+ "cache": (
390
+ Cache()
391
+ if not hasattr(self, "cache")
392
+ else self.cache.to_dict(add_edsl_version=add_edsl_version)
393
+ )
394
+ }
395
+ )
396
+
397
+ if self.task_history.has_unfixed_exceptions or include_task_history:
398
+ d.update({"task_history": self.task_history.to_dict()})
399
+
400
+ if add_edsl_version:
401
+ from edsl import __version__
402
+
403
+ d["edsl_version"] = __version__
404
+ d["edsl_class_name"] = "Results"
405
+
406
+ return d
407
+
408
+ def compare(self, other_results: Results) -> dict:
409
+ """
410
+ Compare two Results objects and return the differences.
411
+ """
412
+ hashes_0 = [hash(result) for result in self]
413
+ hashes_1 = [hash(result) for result in other_results]
414
+
415
+ in_self_but_not_other = set(hashes_0).difference(set(hashes_1))
416
+ in_other_but_not_self = set(hashes_1).difference(set(hashes_0))
417
+
418
+ indicies_self = [hashes_0.index(h) for h in in_self_but_not_other]
419
+ indices_other = [hashes_1.index(h) for h in in_other_but_not_self]
420
+ return {
421
+ "a_not_b": [self[i] for i in indicies_self],
422
+ "b_not_a": [other_results[i] for i in indices_other],
423
+ }
424
+
425
+ @property
426
+ def has_unfixed_exceptions(self) -> bool:
427
+ return self.task_history.has_unfixed_exceptions
428
+
429
+ def __hash__(self) -> int:
430
+ from edsl.utilities.utilities import dict_hash
431
+
432
+ return dict_hash(
433
+ self.to_dict(sort=True, add_edsl_version=False, include_cache_info=False)
434
+ )
435
+
436
+ @property
437
+ def hashes(self) -> set:
438
+ return set(hash(result) for result in self.data)
439
+
440
+ def sample(self, n: int) -> Results:
441
+ """Return a random sample of the results.
442
+
443
+ :param n: The number of samples to return.
444
+
445
+ >>> from edsl.results import Results
446
+ >>> r = Results.example()
447
+ >>> len(r.sample(2))
448
+ 2
449
+ """
450
+ indices = None
451
+
452
+ for entry in self:
453
+ key, values = list(entry.items())[0]
454
+ if indices is None: # gets the indices for the first time
455
+ indices = list(range(len(values)))
456
+ sampled_indices = random.sample(indices, n)
457
+ if n > len(indices):
458
+ raise ResultsError(
459
+ f"Cannot sample {n} items from a list of length {len(indices)}."
460
+ )
461
+ entry[key] = [values[i] for i in sampled_indices]
462
+
463
+ return self
136
464
 
137
465
  @classmethod
466
+ @remove_edsl_version
138
467
  def from_dict(cls, data: dict[str, Any]) -> Results:
139
- """Converts a dictionary to a Results object"""
140
- results = cls(
141
- survey=Survey.from_dict(data["survey"]),
142
- data=[Result.from_dict(r) for r in data["data"]],
143
- created_columns=data.get("created_columns", None),
468
+ """Convert a dictionary to a Results object.
469
+
470
+ :param data: A dictionary representation of a Results object.
471
+
472
+ Example:
473
+
474
+ >>> r = Results.example()
475
+ >>> d = r.to_dict()
476
+ >>> r2 = Results.from_dict(d)
477
+ >>> r == r2
478
+ True
479
+ """
480
+ from edsl.surveys.Survey import Survey
481
+ from edsl.data.Cache import Cache
482
+ from edsl.results.Result import Result
483
+ from edsl.jobs.tasks.TaskHistory import TaskHistory
484
+ from edsl.agents.Agent import Agent
485
+
486
+ survey = Survey.from_dict(data["survey"])
487
+ results_data = [Result.from_dict(r) for r in data["data"]]
488
+ created_columns = data.get("created_columns", None)
489
+ cache = Cache.from_dict(data.get("cache")) if "cache" in data else Cache()
490
+ task_history = (
491
+ TaskHistory.from_dict(data.get("task_history"))
492
+ if "task_history" in data
493
+ else TaskHistory(interviews=[])
144
494
  )
145
- return results
495
+ params = {
496
+ "survey": survey,
497
+ "data": results_data,
498
+ "created_columns": created_columns,
499
+ "cache": cache,
500
+ "task_history": task_history,
501
+ }
146
502
 
147
- def show_methods(self, show_docstrings: bool = True):
148
- """Prints public methods of the Results class"""
149
- print_public_methods_with_doc(self)
503
+ try:
504
+ results = cls(**params)
505
+ except Exception as e:
506
+ raise ResultsDeserializationError(f"Error in Results.from_dict: {e}")
507
+ return results
150
508
 
151
- ######################
152
- ## Convenience methods
153
- ## & Report methods
154
- ######################
155
509
  @property
156
510
  def _key_to_data_type(self) -> dict[str, str]:
157
511
  """
158
- Returns a mapping of keys (how_feeling, status, etc.) to strings representing data types (objects such as Agent, Answer, Model, Scenario, etc.)
512
+ Return a mapping of keys (how_feeling, status, etc.) to strings representing data types.
513
+
514
+ Objects such as Agent, Answer, Model, Scenario, etc.
159
515
  - Uses the key_to_data_type property of the Result class.
160
516
  - Includes any columns that the user has created with `mutate`
161
517
  """
162
- d = {}
518
+ d: dict = {}
163
519
  for result in self.data:
164
520
  d.update(result.key_to_data_type)
165
521
  for column in self.created_columns:
166
522
  d[column] = "answer"
523
+
167
524
  return d
168
525
 
169
526
  @property
170
527
  def _data_type_to_keys(self) -> dict[str, str]:
171
528
  """
172
- Returns a mapping of strings representing data types (objects such as Agent, Answer, Model, Scenario, etc.) to keys (how_feeling, status, etc.)
529
+ Return a mapping of strings representing data types (objects such as Agent, Answer, Model, Scenario, etc.) to keys (how_feeling, status, etc.)
173
530
  - Uses the key_to_data_type property of the Result class.
174
531
  - Includes any columns that the user has created with `mutate`
532
+
533
+ Example:
534
+
535
+ >>> r = Results.example()
536
+ >>> r._data_type_to_keys
537
+ defaultdict(...
175
538
  """
176
- d = defaultdict(set)
539
+ d: dict = defaultdict(set)
177
540
  for result in self.data:
178
541
  for key, value in result.key_to_data_type.items():
179
542
  d[value] = d[value].union(set({key}))
@@ -183,102 +546,305 @@ class Results(UserList, Mixins, Base):
183
546
 
184
547
  @property
185
548
  def columns(self) -> list[str]:
186
- """Returns a list of all of the columns that are in the Results"""
187
- # return list(self.all_keys)
549
+ """Return a list of all of the columns that are in the Results.
550
+
551
+ Example:
552
+
553
+ >>> r = Results.example()
554
+ >>> r.columns
555
+ ['agent.agent_index', ...]
556
+ """
188
557
  column_names = [f"{v}.{k}" for k, v in self._key_to_data_type.items()]
189
- return sorted(column_names)
558
+ from edsl.utilities.PrettyList import PrettyList
559
+
560
+ return PrettyList(sorted(column_names))
190
561
 
191
562
  @property
192
563
  def answer_keys(self) -> dict[str, str]:
193
- """Returns a mapping of answer keys to question text"""
564
+ """Return a mapping of answer keys to question text.
565
+
566
+ Example:
567
+
568
+ >>> r = Results.example()
569
+ >>> r.answer_keys
570
+ {'how_feeling': 'How are you this {{ period }}?', 'how_feeling_yesterday': 'How were you feeling yesterday {{ period }}?'}
571
+ """
572
+ from edsl.utilities.utilities import shorten_string
573
+
574
+ if not self.survey:
575
+ raise ResultsError("Survey is not defined so no answer keys are available.")
576
+
194
577
  answer_keys = self._data_type_to_keys["answer"]
195
578
  answer_keys = {k for k in answer_keys if "_comment" not in k}
196
579
  questions_text = [
197
- self.survey.get_question(k).question_text for k in answer_keys
580
+ self.survey._get_question_by_name(k).question_text for k in answer_keys
198
581
  ]
199
582
  short_question_text = [shorten_string(q, 80) for q in questions_text]
200
- return dict(zip(answer_keys, short_question_text))
583
+ initial_dict = dict(zip(answer_keys, short_question_text))
584
+ sorted_dict = {key: initial_dict[key] for key in sorted(initial_dict)}
585
+ return sorted_dict
201
586
 
202
587
  @property
203
- def agents(self) -> list[Agent]:
204
- return [r.agent for r in self.data]
588
+ def agents(self) -> AgentList:
589
+ """Return a list of all of the agents in the Results.
590
+
591
+ Example:
592
+
593
+ >>> r = Results.example()
594
+ >>> r.agents
595
+ AgentList([Agent(traits = {'status': 'Joyful'}), Agent(traits = {'status': 'Joyful'}), Agent(traits = {'status': 'Sad'}), Agent(traits = {'status': 'Sad'})])
596
+ """
597
+ from edsl.agents.AgentList import AgentList
598
+
599
+ return AgentList([r.agent for r in self.data])
205
600
 
206
601
  @property
207
- def models(self) -> list[Type[LanguageModel]]:
208
- return [r.model for r in self.data]
602
+ def models(self) -> ModelList:
603
+ """Return a list of all of the models in the Results.
604
+
605
+ Example:
606
+
607
+ >>> r = Results.example()
608
+ >>> r.models[0]
609
+ Model(model_name = ...)
610
+ """
611
+ from edsl.language_models.ModelList import ModelList
612
+
613
+ return ModelList([r.model for r in self.data])
614
+
615
+ def __eq__(self, other):
616
+ return hash(self) == hash(other)
209
617
 
210
618
  @property
211
- def scenarios(self) -> list[Scenario]:
212
- return [r.scenario for r in self.data]
619
+ def scenarios(self) -> ScenarioList:
620
+ """Return a list of all of the scenarios in the Results.
621
+
622
+ Example:
623
+
624
+ >>> r = Results.example()
625
+ >>> r.scenarios
626
+ ScenarioList([Scenario({'period': 'morning', 'scenario_index': 0}), Scenario({'period': 'afternoon', 'scenario_index': 1}), Scenario({'period': 'morning', 'scenario_index': 0}), Scenario({'period': 'afternoon', 'scenario_index': 1})])
627
+ """
628
+ from edsl.scenarios.ScenarioList import ScenarioList
629
+
630
+ return ScenarioList([r.scenario for r in self.data])
213
631
 
214
632
  @property
215
- def agent_keys(self) -> set[str]:
216
- """Returns a set of all of the keys that are in the Agent data"""
217
- return self._data_type_to_keys["agent"]
633
+ def agent_keys(self) -> list[str]:
634
+ """Return a set of all of the keys that are in the Agent data.
635
+
636
+ Example:
637
+
638
+ >>> r = Results.example()
639
+ >>> r.agent_keys
640
+ ['agent_index', 'agent_instruction', 'agent_name', 'status']
641
+ """
642
+ return sorted(self._data_type_to_keys["agent"])
218
643
 
219
644
  @property
220
- def model_keys(self) -> set[str]:
221
- """Returns a set of all of the keys that are in the LanguageModel data"""
222
- return self._data_type_to_keys["model"]
645
+ def model_keys(self) -> list[str]:
646
+ """Return a set of all of the keys that are in the LanguageModel data.
647
+
648
+ >>> r = Results.example()
649
+ >>> r.model_keys
650
+ ['frequency_penalty', 'logprobs', 'max_tokens', 'model', 'model_index', 'presence_penalty', 'temperature', 'top_logprobs', 'top_p']
651
+ """
652
+ return sorted(self._data_type_to_keys["model"])
223
653
 
224
654
  @property
225
- def scenario_keys(self) -> set[str]:
226
- """Returns a set of all of the keys that are in the Scenario data"""
227
- return self._data_type_to_keys["scenario"]
655
+ def scenario_keys(self) -> list[str]:
656
+ """Return a set of all of the keys that are in the Scenario data.
657
+
658
+ >>> r = Results.example()
659
+ >>> r.scenario_keys
660
+ ['period', 'scenario_index']
661
+ """
662
+ return sorted(self._data_type_to_keys["scenario"])
228
663
 
229
664
  @property
230
665
  def question_names(self) -> list[str]:
231
- """Returns a list of all of the question names"""
666
+ """Return a list of all of the question names.
667
+
668
+ Example:
669
+
670
+ >>> r = Results.example()
671
+ >>> r.question_names
672
+ ['how_feeling', 'how_feeling_yesterday']
673
+ """
232
674
  if self.survey is None:
233
675
  return []
234
- return list(self.survey.question_names)
676
+ return sorted(list(self.survey.question_names))
235
677
 
236
678
  @property
237
- def all_keys(self) -> set[str]:
238
- """Returns a set of all of the keys that are in the Results"""
679
+ def all_keys(self) -> list[str]:
680
+ """Return a set of all of the keys that are in the Results.
681
+
682
+ Example:
683
+
684
+ >>> r = Results.example()
685
+ >>> r.all_keys
686
+ ['agent_index', ...]
687
+ """
239
688
  answer_keys = set(self.answer_keys)
240
- return (
689
+ all_keys = (
241
690
  answer_keys.union(self.agent_keys)
242
691
  .union(self.scenario_keys)
243
692
  .union(self.model_keys)
244
693
  )
694
+ return sorted(list(all_keys))
695
+
696
+ def first(self) -> Result:
697
+ """Return the first observation in the results.
698
+
699
+ Example:
700
+
701
+ >>> r = Results.example()
702
+ >>> r.first()
703
+ Result(agent...
704
+ """
705
+ return self.data[0]
706
+
707
+ def answer_truncate(
708
+ self, column: str, top_n: int = 5, new_var_name: str = None
709
+ ) -> Results:
710
+ """Create a new variable that truncates the answers to the top_n.
711
+
712
+ :param column: The column to truncate.
713
+ :param top_n: The number of top answers to keep.
714
+ :param new_var_name: The name of the new variable. If None, it is the original name + '_truncated'.
245
715
 
246
- def relevant_columns(self) -> set[str]:
247
- """Returns all of the columns that are in the results."""
248
- return set().union(
249
- *(observation.combined_dict.keys() for observation in self.data)
716
+ Example:
717
+ >>> r = Results.example()
718
+ >>> r.answer_truncate('how_feeling', top_n = 2).select('how_feeling', 'how_feeling_truncated')
719
+ Dataset([{'answer.how_feeling': ['OK', 'Great', 'Terrible', 'OK']}, {'answer.how_feeling_truncated': ['Other', 'Other', 'Other', 'Other']}])
720
+
721
+
722
+ """
723
+ if new_var_name is None:
724
+ new_var_name = column + "_truncated"
725
+ answers = list(self.select(column).tally().keys())
726
+
727
+ def f(x):
728
+ if x in answers[:top_n]:
729
+ return x
730
+ else:
731
+ return "Other"
732
+
733
+ return self.recode(column, recode_function=f, new_var_name=new_var_name)
734
+
735
+ def recode(
736
+ self, column: str, recode_function: Optional[Callable], new_var_name=None
737
+ ) -> Results:
738
+ """
739
+ Recode a column in the Results object.
740
+
741
+ >>> r = Results.example()
742
+ >>> r.recode('how_feeling', recode_function = lambda x: 1 if x == 'Great' else 0).select('how_feeling', 'how_feeling_recoded')
743
+ Dataset([{'answer.how_feeling': ['OK', 'Great', 'Terrible', 'OK']}, {'answer.how_feeling_recoded': [0, 1, 0, 0]}])
744
+ """
745
+
746
+ if new_var_name is None:
747
+ new_var_name = column + "_recoded"
748
+ new_data = []
749
+ for result in self.data:
750
+ new_result = result.copy()
751
+ value = new_result.get_value("answer", column)
752
+ # breakpoint()
753
+ new_result["answer"][new_var_name] = recode_function(value)
754
+ new_data.append(new_result)
755
+
756
+ # print("Created new variable", new_var_name)
757
+ return Results(
758
+ survey=self.survey,
759
+ data=new_data,
760
+ created_columns=self.created_columns + [new_var_name],
250
761
  )
251
762
 
252
- def _parse_column(self, column: str) -> tuple[str, str]:
763
+ def add_column(self, column_name: str, values: list) -> Results:
764
+ """Adds columns to Results
765
+
766
+ >>> r = Results.example()
767
+ >>> r.add_column('a', [1,2,3, 4]).select('a')
768
+ Dataset([{'answer.a': [1, 2, 3, 4]}])
253
769
  """
254
- Parses a column name into a tuple containing a data type and a key.
255
- - Uses the key_to_data_type property of the Results class.
256
- - The standard way a column is specified is with a dot-separated string, e.g. _parse_column("agent.status")
257
- - But you can also specify a single key, e.g. "status", in which case it will look up the data type.
770
+
771
+ assert len(values) == len(
772
+ self.data
773
+ ), "The number of values must match the number of results."
774
+ new_results = self.data.copy()
775
+ for i, result in enumerate(new_results):
776
+ result["answer"][column_name] = values[i]
777
+ return Results(
778
+ survey=self.survey,
779
+ data=new_results,
780
+ created_columns=self.created_columns + [column_name],
781
+ )
782
+
783
+ def add_columns_from_dict(self, columns: List[dict]) -> Results:
784
+ """Adds columns to Results from a list of dictionaries.
785
+
786
+ >>> r = Results.example()
787
+ >>> r.add_columns_from_dict([{'a': 1, 'b': 2}, {'a': 3, 'b': 4}, {'a':3, 'b':2}, {'a':3, 'b':2}]).select('a', 'b')
788
+ Dataset([{'answer.a': [1, 3, 3, 3]}, {'answer.b': [2, 4, 2, 2]}])
258
789
  """
259
- if "." in column:
260
- data_type, key = column.split(".")
261
- else:
262
- try:
263
- data_type, key = self._key_to_data_type[column], column
264
- except KeyError:
265
- raise ResultsColumnNotFoundError(f"Column {column} not found in data")
266
- return data_type, key
790
+ keys = list(columns[0].keys())
791
+ for key in keys:
792
+ values = [d[key] for d in columns]
793
+ self = self.add_column(key, values)
794
+ return self
795
+
796
+ @staticmethod
797
+ def _create_evaluator(
798
+ result: Result, functions_dict: Optional[dict] = None
799
+ ) -> "EvalWithCompoundTypes":
800
+ """Create an evaluator for the expression.
801
+
802
+ >>> from unittest.mock import Mock
803
+ >>> result = Mock()
804
+ >>> result.combined_dict = {'how_feeling': 'OK'}
805
+
806
+ >>> evaluator = Results._create_evaluator(result = result, functions_dict = {})
807
+ >>> evaluator.eval("how_feeling == 'OK'")
808
+ True
809
+
810
+ >>> result.combined_dict = {'answer': {'how_feeling': 'OK'}}
811
+ >>> evaluator = Results._create_evaluator(result = result, functions_dict = {})
812
+ >>> evaluator.eval("answer.how_feeling== 'OK'")
813
+ True
814
+
815
+ Note that you need to refer to the answer dictionary in the expression.
816
+
817
+ >>> evaluator.eval("how_feeling== 'OK'")
818
+ Traceback (most recent call last):
819
+ ...
820
+ simpleeval.NameNotDefined: 'how_feeling' is not defined for expression 'how_feeling== 'OK''
821
+ """
822
+ from simpleeval import EvalWithCompoundTypes
267
823
 
268
- def first(self) -> Result:
269
- """Returns the first observation in the results."""
270
- return self.data[0]
824
+ if functions_dict is None:
825
+ functions_dict = {}
826
+ evaluator = EvalWithCompoundTypes(
827
+ names=result.combined_dict, functions=functions_dict
828
+ )
829
+ evaluator.functions.update(int=int, float=float)
830
+ return evaluator
271
831
 
272
- def mutate(self, new_var_string: str, functions_dict: dict = None) -> Results:
832
+ def mutate(
833
+ self, new_var_string: str, functions_dict: Optional[dict] = None
834
+ ) -> Results:
273
835
  """
274
836
  Creates a value in the Results object as if has been asked as part of the survey.
275
- - It splits the new_var_string at the "=" and uses simple_eval
276
- - The functions dict is...
837
+
838
+ :param new_var_string: A string that is a valid Python expression.
839
+ :param functions_dict: A dictionary of functions that can be used in the expression. The keys are the function names and the values are the functions themselves.
840
+
841
+ It splits the new_var_string at the "=" and uses simple_eval
277
842
 
278
843
  Example:
279
- >>> r = Results.create_example()
844
+
845
+ >>> r = Results.example()
280
846
  >>> r.mutate('how_feeling_x = how_feeling + "x"').select('how_feeling_x')
281
- [{'answer.how_feeling_x': ['Badx', 'Badx', 'Greatx', 'Greatx']}]
847
+ Dataset([{'answer.how_feeling_x': ...
282
848
  """
283
849
  # extract the variable name and the expression
284
850
  if "=" not in new_var_string:
@@ -287,19 +853,16 @@ class Results(UserList, Mixins, Base):
287
853
  )
288
854
  raw_var_name, expression = new_var_string.split("=", 1)
289
855
  var_name = raw_var_name.strip()
856
+ from edsl.utilities.utilities import is_valid_variable_name
857
+
290
858
  if not is_valid_variable_name(var_name):
291
859
  raise ResultsInvalidNameError(f"{var_name} is not a valid variable name.")
292
860
 
293
861
  # create the evaluator
294
862
  functions_dict = functions_dict or {}
295
863
 
296
- def create_evaluator(result: Result) -> EvalWithCompoundTypes:
297
- return EvalWithCompoundTypes(
298
- names=result.combined_dict, functions=functions_dict
299
- )
300
-
301
- def new_result(old_result: Result, var_name: str) -> Result:
302
- evaluator = create_evaluator(old_result)
864
+ def new_result(old_result: "Result", var_name: str) -> "Result":
865
+ evaluator = self._create_evaluator(old_result, functions_dict)
303
866
  value = evaluator.eval(expression)
304
867
  new_result = old_result.copy()
305
868
  new_result["answer"][var_name] = value
@@ -316,83 +879,164 @@ class Results(UserList, Mixins, Base):
316
879
  created_columns=self.created_columns + [var_name],
317
880
  )
318
881
 
319
- def select(self, *columns: Union[str, list[str]]) -> Dataset:
882
+ def add_column(self, column_name: str, values: list) -> Results:
883
+ """Adds columns to Results
884
+
885
+ >>> r = Results.example()
886
+ >>> r.add_column('a', [1,2,3, 4]).select('a')
887
+ Dataset([{'answer.a': [1, 2, 3, 4]}])
320
888
  """
321
- This selects data from the results and turns it into a format like so:
322
- >>> results = Results.create_example()
323
- >>> results.select('how_feeling')
324
- [{'answer.how_feeling': ['Bad', 'Bad', 'Great', 'Great']}]
889
+
890
+ assert len(values) == len(
891
+ self.data
892
+ ), "The number of values must match the number of results."
893
+ new_results = self.data.copy()
894
+ for i, result in enumerate(new_results):
895
+ result["answer"][column_name] = values[i]
896
+ return Results(
897
+ survey=self.survey,
898
+ data=new_results,
899
+ created_columns=self.created_columns + [column_name],
900
+ )
901
+
902
+ def rename(self, old_name: str, new_name: str) -> Results:
903
+ """Rename an answer column in a Results object.
904
+
905
+ >>> s = Results.example()
906
+ >>> s.rename('how_feeling', 'how_feeling_new').select('how_feeling_new')
907
+ Dataset([{'answer.how_feeling_new': ['OK', 'Great', 'Terrible', 'OK']}])
908
+
909
+ # TODO: Should we allow renaming of scenario fields as well? Probably.
910
+
325
911
  """
326
912
 
327
- if not columns or columns == ("*",) or columns == (None,):
328
- columns = ("*.*",)
913
+ for obs in self.data:
914
+ obs["answer"][new_name] = obs["answer"][old_name]
915
+ del obs["answer"][old_name]
329
916
 
330
- if isinstance(columns[0], list):
331
- columns = tuple(columns[0])
917
+ return self
332
918
 
333
- known_data_types = [
334
- "answer",
335
- "scenario",
336
- "agent",
337
- "model",
338
- "prompt",
339
- "raw_model_response",
340
- ]
919
+ def shuffle(self, seed: Optional[str] = "edsl") -> Results:
920
+ """Shuffle the results.
341
921
 
342
- def get_data_types_to_return(parsed_data_type):
343
- if parsed_data_type == "*": # they want all of the columns
344
- return known_data_types
345
- else:
346
- if parsed_data_type not in known_data_types:
347
- raise Exception(
348
- f"Data type {parsed_data_type} not found in data. Did you mean one of {known_data_types}"
349
- )
350
- return [parsed_data_type]
922
+ Example:
351
923
 
352
- # we're doing to populate this with the data we want to fetch
353
- to_fetch = defaultdict(list)
924
+ >>> r = Results.example()
925
+ >>> r.shuffle(seed = 1)[0]
926
+ Result(...)
927
+ """
928
+ if seed != "edsl":
929
+ seed = random.seed(seed)
354
930
 
355
- new_data = []
356
- items_in_order = []
357
- # iterate through the passed columns
358
- for column in columns:
359
- # a user could pass 'result.how_feeling' or just 'how_feeling'
360
- parsed_data_type, parsed_key = self._parse_column(column)
361
- data_types = get_data_types_to_return(parsed_data_type)
362
- found_once = False # we need to track this to make sure we found the key at least once
931
+ new_data = self.data.copy()
932
+ random.shuffle(new_data)
933
+ return Results(survey=self.survey, data=new_data, created_columns=None)
934
+
935
+ def sample(
936
+ self,
937
+ n: Optional[int] = None,
938
+ frac: Optional[float] = None,
939
+ with_replacement: bool = True,
940
+ seed: Optional[str] = None,
941
+ ) -> Results:
942
+ """Sample the results.
943
+
944
+ :param n: An integer representing the number of samples to take.
945
+ :param frac: A float representing the fraction of samples to take.
946
+ :param with_replacement: A boolean representing whether to sample with replacement.
947
+ :param seed: An integer representing the seed for the random number generator.
948
+
949
+ Example:
950
+
951
+ >>> r = Results.example()
952
+ >>> len(r.sample(2))
953
+ 2
954
+ """
955
+ if seed:
956
+ random.seed(seed)
957
+
958
+ if n is None and frac is None:
959
+ raise Exception("You must specify either n or frac.")
960
+
961
+ if n is not None and frac is not None:
962
+ raise Exception("You cannot specify both n and frac.")
963
+
964
+ if frac is not None and n is None:
965
+ n = int(frac * len(self.data))
966
+
967
+ if with_replacement:
968
+ new_data = random.choices(self.data, k=n)
969
+ else:
970
+ new_data = random.sample(self.data, n)
971
+
972
+ return Results(survey=self.survey, data=new_data, created_columns=None)
973
+
974
+ def select(self, *columns: Union[str, list[str]]) -> Results:
975
+ """
976
+ Select data from the results and format it.
977
+
978
+ :param columns: A list of strings, each of which is a column name. The column name can be a single key, e.g. "how_feeling", or a dot-separated string, e.g. "answer.how_feeling".
979
+
980
+ Example:
981
+
982
+ >>> results = Results.example()
983
+ >>> results.select('how_feeling')
984
+ Dataset([{'answer.how_feeling': ['OK', 'Great', 'Terrible', 'OK']}])
985
+
986
+ >>> results.select('how_feeling', 'model', 'how_feeling')
987
+ Dataset([{'answer.how_feeling': ['OK', 'Great', 'Terrible', 'OK']}, {'answer.how_feeling': ['OK', 'Great', 'Terrible', 'OK']}, {'model.model': ['...', '...', '...', '...']}, {'answer.how_feeling': ['OK', 'Great', 'Terrible', 'OK']}, {'answer.how_feeling': ['OK', 'Great', 'Terrible', 'OK']}])
988
+
989
+ >>> from edsl import Results; r = Results.example(); r.select('answer.how_feeling_y')
990
+ Dataset([{'answer.how_feeling_yesterday': ['Great', 'Good', 'OK', 'Terrible']}])
991
+ """
363
992
 
364
- for data_type in data_types:
365
- # the keys for that data_type e.g.,# if data_type is 'answer', then the keys are 'how_feeling', 'how_feeling_comment', etc.
366
- relevant_keys = self._data_type_to_keys[data_type]
993
+ from edsl.results.results_selector import Selector
367
994
 
368
- for key in relevant_keys:
369
- if key == parsed_key or parsed_key == "*":
370
- found_once = True
371
- to_fetch[data_type].append(key)
372
- items_in_order.append(data_type + "." + key)
995
+ if len(self) == 0:
996
+ raise Exception("No data to select from---the Results object is empty.")
373
997
 
374
- if not found_once:
375
- raise Exception(f"Key {parsed_key} not found in data.")
998
+ selector = Selector(
999
+ known_data_types=self.known_data_types,
1000
+ data_type_to_keys=self._data_type_to_keys,
1001
+ key_to_data_type=self._key_to_data_type,
1002
+ fetch_list_func=self._fetch_list,
1003
+ columns=self.columns,
1004
+ )
1005
+ return selector.select(*columns)
1006
+
1007
+ def sort_by(self, *columns: str, reverse: bool = False) -> Results:
1008
+ """Sort the results by one or more columns."""
1009
+ import warnings
1010
+
1011
+ warnings.warn(
1012
+ "sort_by is deprecated. Use order_by instead.", DeprecationWarning
1013
+ )
1014
+ return self.order_by(*columns, reverse=reverse)
1015
+
1016
+ def _parse_column(self, column: str) -> tuple[str, str]:
1017
+ """Parse a column name into a data type and key."""
1018
+ if "." in column:
1019
+ return column.split(".")
1020
+ return self._key_to_data_type[column], column
376
1021
 
377
- for data_type in to_fetch:
378
- for key in to_fetch[data_type]:
379
- entries = self._fetch_list(data_type, key)
380
- new_data.append({data_type + "." + key: entries})
1022
+ def order_by(self, *columns: str, reverse: bool = False) -> Results:
1023
+ """Sort the results by one or more columns.
381
1024
 
382
- def sort_by_key_order(dictionary):
383
- # Extract the single key from the dictionary
384
- single_key = next(iter(dictionary))
385
- # Return the index of this key in the list_of_keys
386
- return items_in_order.index(single_key)
1025
+ :param columns: One or more column names as strings.
1026
+ :param reverse: A boolean that determines whether to sort in reverse order.
387
1027
 
388
- sorted(new_data, key=sort_by_key_order)
1028
+ Each column name can be a single key, e.g. "how_feeling", or a dot-separated string, e.g. "answer.how_feeling".
389
1029
 
390
- return Dataset(new_data)
1030
+ Example:
391
1031
 
392
- def sort_by(self, column, reverse=True) -> Results:
393
- "Sorts the results by a column"
1032
+ >>> r = Results.example()
1033
+ >>> r.sort_by('how_feeling', reverse=False).select('how_feeling')
1034
+ Dataset([{'answer.how_feeling': ['Great', 'OK', 'OK', 'Terrible']}])
394
1035
 
395
- data_type, key = self._parse_column(column)
1036
+ >>> r.sort_by('how_feeling', reverse=True).select('how_feeling')
1037
+ Dataset([{'answer.how_feeling': ['Terrible', 'OK', 'OK', 'Great']}])
1038
+
1039
+ """
396
1040
 
397
1041
  def to_numeric_if_possible(v):
398
1042
  try:
@@ -400,66 +1044,144 @@ class Results(UserList, Mixins, Base):
400
1044
  except:
401
1045
  return v
402
1046
 
403
- new_data = sorted(
404
- self.data,
405
- key=lambda x: to_numeric_if_possible(x.get_value(data_type, key)),
406
- reverse=reverse,
407
- )
1047
+ def sort_key(item):
1048
+ key_components = []
1049
+ for col in columns:
1050
+ data_type, key = self._parse_column(col)
1051
+ value = item.get_value(data_type, key)
1052
+ key_components.append(to_numeric_if_possible(value))
1053
+ return tuple(key_components)
1054
+
1055
+ new_data = sorted(self.data, key=sort_key, reverse=reverse)
408
1056
  return Results(survey=self.survey, data=new_data, created_columns=None)
409
1057
 
410
- def filter(self, expression) -> Results:
1058
+ def filter(self, expression: str) -> Results:
411
1059
  """
412
- This filters a result based on the expression that is passed in.
413
- >>> r = Results.create_example()
1060
+ Filter based on the given expression and returns the filtered `Results`.
1061
+
1062
+ :param expression: A string expression that evaluates to a boolean. The expression is applied to each element in `Results` to determine whether it should be included in the filtered results.
1063
+
1064
+ The `expression` parameter is a string that must resolve to a boolean value when evaluated against each element in `Results`.
1065
+ This expression is used to determine which elements to include in the returned `Results`.
1066
+
1067
+ Example usage: Create an example `Results` instance and apply filters to it:
1068
+
1069
+ >>> r = Results.example()
414
1070
  >>> r.filter("how_feeling == 'Great'").select('how_feeling')
415
- [{'answer.how_feeling': ['Great', 'Great']}]
416
- >>> r.filter("how_feeling == 'Nothing'").select('how_feeling')
417
- [{'answer.how_feeling': []}]
1071
+ Dataset([{'answer.how_feeling': ['Great']}])
1072
+
1073
+ Example usage: Using an OR operator in the filter expression.
1074
+
1075
+ >>> r = Results.example().filter("how_feeling = 'Great'").select('how_feeling')
1076
+ Traceback (most recent call last):
1077
+ ...
1078
+ edsl.exceptions.results.ResultsFilterError: You must use '==' instead of '=' in the filter expression.
1079
+ ...
1080
+
1081
+ >>> r.filter("how_feeling == 'Great' or how_feeling == 'Terrible'").select('how_feeling')
1082
+ Dataset([{'answer.how_feeling': ['Great', 'Terrible']}])
418
1083
  """
419
1084
 
420
- def create_evaluator(result):
421
- return EvalWithCompoundTypes(names=result.combined_dict)
1085
+ def has_single_equals(string):
1086
+ if "!=" in string:
1087
+ return False
1088
+ if "=" in string and not (
1089
+ "==" in string or "<=" in string or ">=" in string
1090
+ ):
1091
+ return True
1092
+
1093
+ if has_single_equals(expression):
1094
+ raise ResultsFilterError(
1095
+ "You must use '==' instead of '=' in the filter expression."
1096
+ )
422
1097
 
423
1098
  try:
424
- new_data = [
425
- result
426
- for result in self.data
427
- if create_evaluator(result).eval(expression)
428
- ]
1099
+ # iterates through all the results and evaluates the expression
1100
+ new_data = []
1101
+ for result in self.data:
1102
+ evaluator = self._create_evaluator(result)
1103
+ result.check_expression(expression) # check expression
1104
+ if evaluator.eval(expression):
1105
+ new_data.append(result)
1106
+
1107
+ except ValueError as e:
1108
+ raise ResultsFilterError(
1109
+ f"Error in filter. Exception:{e}",
1110
+ f"The expression you provided was: {expression}",
1111
+ "See https://docs.expectedparrot.com/en/latest/results.html#filtering-results for more details.",
1112
+ )
429
1113
  except Exception as e:
430
- print(f"Exception:{e}")
1114
+ raise ResultsFilterError(
1115
+ f"""Error in filter. Exception:{e}.""",
1116
+ f"""The expression you provided was: {expression}.""",
1117
+ """Please make sure that the expression is a valid Python expression that evaluates to a boolean.""",
1118
+ """For example, 'how_feeling == "Great"' is a valid expression, as is 'how_feeling in ["Great", "Terrible"]'., """,
1119
+ """However, 'how_feeling = "Great"' is not a valid expression.""",
1120
+ """See https://docs.expectedparrot.com/en/latest/results.html#filtering-results for more details.""",
1121
+ )
1122
+
1123
+ if len(new_data) == 0:
1124
+ import warnings
1125
+
1126
+ warnings.warn("No results remain after applying the filter.")
431
1127
 
432
1128
  return Results(survey=self.survey, data=new_data, created_columns=None)
433
1129
 
434
1130
  @classmethod
435
- def example(cls, debug: bool = False) -> Results:
436
- """
437
- Returns an example Results object
438
- - debug: if False, uses actual API calls
439
- """
440
- from edsl.jobs import Jobs
1131
+ def example(cls, randomize: bool = False) -> Results:
1132
+ """Return an example `Results` object.
1133
+
1134
+ Example usage:
1135
+
1136
+ >>> r = Results.example()
441
1137
 
442
- job = Jobs.example()
443
- results = job.run()
1138
+ :param debug: if False, uses actual API calls
1139
+ """
1140
+ from edsl.jobs.Jobs import Jobs
1141
+ from edsl.data.Cache import Cache
1142
+
1143
+ c = Cache()
1144
+ job = Jobs.example(randomize=randomize)
1145
+ results = job.run(
1146
+ cache=c,
1147
+ stop_on_exception=True,
1148
+ skip_retry=True,
1149
+ raise_validation_errors=True,
1150
+ disable_remote_cache=True,
1151
+ disable_remote_inference=True,
1152
+ )
444
1153
  return results
445
1154
 
446
1155
  def rich_print(self):
447
- """Displays an object as a table."""
448
- with io.StringIO() as buf:
449
- console = Console(file=buf, record=True)
1156
+ """Display an object as a table."""
1157
+ pass
450
1158
 
451
- for index, result in enumerate(self):
452
- console.print(f"Result {index}")
453
- console.print(result.rich_print())
1159
+ def __str__(self):
1160
+ data = self.to_dict()["data"]
1161
+ return json.dumps(data, indent=4)
454
1162
 
455
- return console.export_text()
1163
+ def show_exceptions(self, traceback=False):
1164
+ """Print the exceptions."""
1165
+ if hasattr(self, "task_history"):
1166
+ self.task_history.show_exceptions(traceback)
1167
+ else:
1168
+ print("No exceptions to show.")
456
1169
 
457
- def __str__(self):
458
- return self.rich_print()
1170
+ def score(self, f: Callable) -> list:
1171
+ """Score the results using in a function.
1172
+
1173
+ :param f: A function that takes values from a Resul object and returns a score.
1174
+
1175
+ >>> r = Results.example()
1176
+ >>> def f(status): return 1 if status == 'Joyful' else 0
1177
+ >>> r.score(f)
1178
+ [1, 1, 0, 0]
1179
+ """
1180
+ return [r.score(f) for r in self.data]
459
1181
 
460
1182
 
461
1183
  def main(): # pragma: no cover
462
- """Calls the OpenAI API credits"""
1184
+ """Call the OpenAI API credits."""
463
1185
  from edsl.results.Results import Results
464
1186
 
465
1187
  results = Results.example(debug=True)
@@ -468,10 +1190,6 @@ def main(): # pragma: no cover
468
1190
 
469
1191
 
470
1192
  if __name__ == "__main__":
471
- print(Results.example())
472
-
473
- r = Results.example()
474
- # db_row = list(r[0].rows(1))
475
- db_rows = list(r.rows())
1193
+ import doctest
476
1194
 
477
- print(r.columns)
1195
+ doctest.testmod(optionflags=doctest.ELLIPSIS)