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
@@ -0,0 +1,98 @@
1
+ from typing import Optional, TYPE_CHECKING, Protocol
2
+ import sys
3
+ from edsl.scenarios.FileStore import HTMLFileStore
4
+ from edsl.config import CONFIG
5
+ from edsl.coop.coop import Coop
6
+
7
+
8
+ class ResultsProtocol(Protocol):
9
+ """Protocol defining the required interface for Results objects."""
10
+
11
+ @property
12
+ def has_unfixed_exceptions(self) -> bool: ...
13
+
14
+ @property
15
+ def task_history(self) -> "TaskHistoryProtocol": ...
16
+
17
+
18
+ class TaskHistoryProtocol(Protocol):
19
+ """Protocol defining the required interface for TaskHistory objects."""
20
+
21
+ @property
22
+ def indices(self) -> list: ...
23
+
24
+ def html(self, cta: str, open_in_browser: bool, return_link: bool) -> str: ...
25
+
26
+
27
+ class RunParametersProtocol(Protocol):
28
+ """Protocol defining the required interface for RunParameters objects."""
29
+
30
+ @property
31
+ def print_exceptions(self) -> bool: ...
32
+
33
+
34
+ class ResultsExceptionsHandler:
35
+ """Handles exception reporting and display functionality."""
36
+
37
+ def __init__(
38
+ self, results: ResultsProtocol, parameters: RunParametersProtocol
39
+ ) -> None:
40
+ self.results = results
41
+ self.parameters = parameters
42
+
43
+ self.open_in_browser = self._get_browser_setting()
44
+ self.remote_logging = self._get_remote_logging_setting()
45
+
46
+ def _get_browser_setting(self) -> bool:
47
+ """Determine if exceptions should be opened in browser based on config."""
48
+ setting = CONFIG.get("EDSL_OPEN_EXCEPTION_REPORT_URL")
49
+ if setting == "True":
50
+ return True
51
+ elif setting == "False":
52
+ return False
53
+ else:
54
+ raise Exception(
55
+ "EDSL_OPEN_EXCEPTION_REPORT_URL must be either True or False"
56
+ )
57
+
58
+ def _get_remote_logging_setting(self) -> bool:
59
+ """Get remote logging setting from coop."""
60
+ try:
61
+ coop = Coop()
62
+ return coop.edsl_settings["remote_logging"]
63
+ except Exception as e:
64
+ # print(e)
65
+ return False
66
+
67
+ def _generate_error_message(self, indices) -> str:
68
+ """Generate appropriate error message based on number of exceptions."""
69
+ msg = f"Exceptions were raised in {len(indices)} interviews.\n"
70
+ if len(indices) > 5:
71
+ msg += f"Exceptions were raised in the following interviews: {indices}.\n"
72
+ return msg
73
+
74
+ def handle_exceptions(self) -> None:
75
+ """Handle exceptions by printing messages and generating reports as needed."""
76
+ if not (
77
+ self.results.has_unfixed_exceptions and self.parameters.print_exceptions
78
+ ):
79
+ return
80
+
81
+ # Print error message
82
+ error_msg = self._generate_error_message(self.results.task_history.indices)
83
+ print(error_msg, file=sys.stderr)
84
+
85
+ # Generate HTML report
86
+ filepath = self.results.task_history.html(
87
+ cta="Open report to see details.",
88
+ open_in_browser=self.open_in_browser,
89
+ return_link=True,
90
+ )
91
+
92
+ # Handle remote logging if enabled
93
+ if self.remote_logging:
94
+ filestore = HTMLFileStore(filepath)
95
+ coop_details = filestore.push(description="Error report")
96
+ print(coop_details)
97
+
98
+ print("Also see: https://docs.expectedparrot.com/en/latest/exceptions.html")
@@ -1,122 +1,163 @@
1
+ from __future__ import annotations
1
2
  import time
2
3
  import asyncio
3
- from typing import Coroutine, List, AsyncGenerator
4
+ import threading
5
+ import warnings
6
+ from typing import TYPE_CHECKING
4
7
 
5
- from rich.live import Live
6
- from rich.console import Console
7
-
8
- from edsl.results import Results, Result
9
- from edsl.jobs.JobsRunner import JobsRunner
10
- from edsl.jobs.Interview import Interview
8
+ from edsl.results.Results import Results
9
+ from edsl.jobs.runners.JobsRunnerStatus import JobsRunnerStatus
10
+ from edsl.jobs.tasks.TaskHistory import TaskHistory
11
11
  from edsl.utilities.decorators import jupyter_nb_handler
12
+ from edsl.jobs.async_interview_runner import AsyncInterviewRunner
13
+ from edsl.jobs.data_structures import RunEnvironment, RunParameters, RunConfig
12
14
 
13
- from edsl.jobs.JobsRunnerStatusMixin import JobsRunnerStatusMixin
14
-
15
-
16
- class JobsRunnerAsyncio(JobsRunner, JobsRunnerStatusMixin):
17
- runner_name = "asyncio"
18
-
19
- async def run_async(
20
- self, n=1, verbose=False, sleep=0, debug=False, progress_bar=False
21
- ) -> AsyncGenerator[Result, None]:
22
- """Creates the tasks, runs them asynchronously, and returns the results as a Results object.
23
- Completed tasks are yielded as they are completed.
24
- """
25
- tasks = self._create_all_interview_tasks(self.interviews, debug)
26
- for task in asyncio.as_completed(tasks):
27
- result = await task
28
- yield result
29
-
30
- def _create_all_interview_tasks(self, interviews, debug) -> List[asyncio.Task]:
31
- """Creates an awaitable task for each interview."""
32
- tasks = []
33
- for i, interview in enumerate(interviews):
34
- interviewing_task = self._interview_task(interview, i, debug)
35
- tasks.append(asyncio.create_task(interviewing_task))
36
- return tasks
37
-
38
- async def _interview_task(
39
- self, interview: Interview, i: int, debug: bool
40
- ) -> Result:
41
- """Conducts an interview and returns the result."""
42
- # the model buckets are used to track usage rates
43
- model_buckets = self.bucket_collection[interview.model]
44
-
45
- # get the results of the interview
46
- answer, valid_results = await interview.async_conduct_interview(
47
- debug=debug, model_buckets=model_buckets
48
- )
49
- # breakpoint()
50
-
51
- # we should have a valid result for each question
52
- answer_key_names = {k for k in set(answer.keys()) if not k.endswith("_comment")}
53
- assert len(valid_results) == len(answer_key_names)
54
-
55
- question_name_to_prompts = dict({})
56
- for result in valid_results:
57
- question_name = result["question_name"]
58
- question_name_to_prompts[question_name] = {
59
- "user_prompt": result["prompts"]["user_prompt"],
60
- "system_prompt": result["prompts"]["system_prompt"],
61
- }
62
-
63
- prompt_dictionary = {}
64
- for answer_key_name in answer_key_names:
65
- prompt_dictionary[
66
- answer_key_name + "_user_prompt"
67
- ] = question_name_to_prompts[answer_key_name]["user_prompt"]
68
- prompt_dictionary[
69
- answer_key_name + "_system_prompt"
70
- ] = question_name_to_prompts[answer_key_name]["system_prompt"]
71
-
72
- raw_model_results_dictionary = {}
73
- for result in valid_results:
74
- question_name = result["question_name"]
75
- raw_model_results_dictionary[
76
- question_name + "_raw_model_response"
77
- ] = result["raw_model_response"]
78
-
79
- result = Result(
80
- agent=interview.agent,
81
- scenario=interview.scenario,
82
- model=interview.model,
83
- iteration=i,
84
- answer=answer,
85
- prompt=prompt_dictionary,
86
- raw_model_response=raw_model_results_dictionary,
87
- )
88
- return result
15
+ if TYPE_CHECKING:
16
+ from edsl.jobs.Jobs import Jobs
89
17
 
90
- @jupyter_nb_handler
91
- async def run(
92
- self, n=1, verbose=True, sleep=0, debug=False, progress_bar=False
93
- ) -> Coroutine:
94
- """Runs a collection of interviews, handling both async and sync contexts."""
95
- verbose = True
96
- console = Console()
18
+
19
+ class JobsRunnerAsyncio:
20
+ """A class for running a collection of interviews asynchronously.
21
+
22
+ It gets instaniated from a Jobs object.
23
+ The Jobs object is a collection of interviews that are to be run.
24
+ """
25
+
26
+ def __init__(self, jobs: "Jobs", environment: RunEnvironment):
27
+ self.jobs = jobs
28
+ self.environment = environment
29
+
30
+ def __len__(self):
31
+ return len(self.jobs)
32
+
33
+ async def run_async(self, parameters: RunParameters) -> Results:
34
+ """Used for some other modules that have a non-standard way of running interviews."""
35
+
36
+ self.environment.jobs_runner_status = JobsRunnerStatus(self, n=parameters.n)
97
37
  data = []
98
- start_time = time.monotonic()
99
-
100
- live = None
101
- if progress_bar:
102
- live = Live(
103
- self._generate_status_table(data, 0),
104
- console=console,
105
- refresh_per_second=10,
106
- )
107
- live.__enter__() # Manually enter the Live context
108
-
109
- async for result in self.run_async(n, verbose, sleep, debug, progress_bar):
110
- end_time = time.monotonic()
111
- elapsed_time = end_time - start_time
38
+ task_history = TaskHistory(include_traceback=False)
39
+
40
+ run_config = RunConfig(parameters=parameters, environment=self.environment)
41
+ result_generator = AsyncInterviewRunner(self.jobs, run_config)
42
+
43
+ async for result, interview in result_generator.run():
112
44
  data.append(result)
45
+ task_history.add_interview(interview)
113
46
 
114
- if progress_bar:
115
- live.update(self._generate_status_table(data, elapsed_time))
47
+ results = Results(survey=self.jobs.survey, task_history=task_history, data=data)
116
48
 
117
- if progress_bar:
118
- live.update(self._generate_status_table(data, elapsed_time))
119
- await asyncio.sleep(0.5) # short delay to show the final status
120
- live.__exit__(None, None, None) # Manually exit the Live context
49
+ relevant_cache = results.relevant_cache(self.environment.cache)
121
50
 
51
+ return Results(
52
+ survey=self.jobs.survey,
53
+ task_history=task_history,
54
+ data=data,
55
+ cache=relevant_cache,
56
+ )
57
+
58
+ def simple_run(self):
59
+ data = asyncio.run(self.run_async())
122
60
  return Results(survey=self.jobs.survey, data=data)
61
+
62
+ @jupyter_nb_handler
63
+ async def run(self, parameters: RunParameters) -> Results:
64
+ """Runs a collection of interviews, handling both async and sync contexts."""
65
+
66
+ run_config = RunConfig(parameters=parameters, environment=self.environment)
67
+
68
+ self.start_time = time.monotonic()
69
+ self.completed = False
70
+
71
+ from edsl.coop import Coop
72
+
73
+ coop = Coop()
74
+ endpoint_url = coop.get_progress_bar_url()
75
+
76
+ def set_up_jobs_runner_status(jobs_runner_status):
77
+ if jobs_runner_status is not None:
78
+ return jobs_runner_status(
79
+ self,
80
+ n=parameters.n,
81
+ endpoint_url=endpoint_url,
82
+ job_uuid=parameters.job_uuid,
83
+ )
84
+ else:
85
+ return JobsRunnerStatus(
86
+ self,
87
+ n=parameters.n,
88
+ endpoint_url=endpoint_url,
89
+ job_uuid=parameters.job_uuid,
90
+ )
91
+
92
+ run_config.environment.jobs_runner_status = set_up_jobs_runner_status(
93
+ self.environment.jobs_runner_status
94
+ )
95
+
96
+ async def get_results(results) -> None:
97
+ """Conducted the interviews and append to the results list."""
98
+ result_generator = AsyncInterviewRunner(self.jobs, run_config)
99
+ async for result, interview in result_generator.run():
100
+ results.append(result)
101
+ results.task_history.add_interview(interview)
102
+
103
+ self.completed = True
104
+
105
+ def run_progress_bar(stop_event, jobs_runner_status) -> None:
106
+ """Runs the progress bar in a separate thread."""
107
+ jobs_runner_status.update_progress(stop_event)
108
+
109
+ def set_up_progress_bar(progress_bar: bool, jobs_runner_status):
110
+ progress_thread = None
111
+ if progress_bar and jobs_runner_status.has_ep_api_key():
112
+ jobs_runner_status.setup()
113
+ progress_thread = threading.Thread(
114
+ target=run_progress_bar, args=(stop_event, jobs_runner_status)
115
+ )
116
+ progress_thread.start()
117
+ elif progress_bar:
118
+ warnings.warn(
119
+ "You need an Expected Parrot API key to view job progress bars."
120
+ )
121
+ return progress_thread
122
+
123
+ results = Results(
124
+ survey=self.jobs.survey,
125
+ data=[],
126
+ task_history=TaskHistory(),
127
+ # cache=self.environment.cache.new_entries_cache(),
128
+ )
129
+
130
+ stop_event = threading.Event()
131
+ progress_thread = set_up_progress_bar(
132
+ parameters.progress_bar, run_config.environment.jobs_runner_status
133
+ )
134
+
135
+ exception_to_raise = None
136
+ try:
137
+ await get_results(results)
138
+ except KeyboardInterrupt:
139
+ print("Keyboard interrupt received. Stopping gracefully...")
140
+ stop_event.set()
141
+ except Exception as e:
142
+ if parameters.stop_on_exception:
143
+ exception_to_raise = e
144
+ stop_event.set()
145
+ finally:
146
+ stop_event.set()
147
+ if progress_thread is not None:
148
+ progress_thread.join()
149
+
150
+ if exception_to_raise:
151
+ raise exception_to_raise
152
+
153
+ relevant_cache = results.relevant_cache(self.environment.cache)
154
+ results.cache = relevant_cache
155
+ # breakpoint()
156
+ results.bucket_collection = self.environment.bucket_collection
157
+
158
+ from edsl.jobs.results_exceptions_handler import ResultsExceptionsHandler
159
+
160
+ results_exceptions_handler = ResultsExceptionsHandler(results, parameters)
161
+
162
+ results_exceptions_handler.handle_exceptions()
163
+ return results
@@ -0,0 +1,298 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import time
5
+ import requests
6
+ from abc import ABC, abstractmethod
7
+ from dataclasses import dataclass
8
+ from collections import defaultdict
9
+ from typing import Any, Dict, Optional
10
+ from uuid import UUID
11
+
12
+
13
+ @dataclass
14
+ class ModelInfo:
15
+ model_name: str
16
+ TPM_limit_k: float
17
+ RPM_limit_k: float
18
+ num_tasks_waiting: int
19
+ token_usage_info: dict
20
+
21
+
22
+ class StatisticsTracker:
23
+ def __init__(self, total_interviews: int, distinct_models: list[str]):
24
+ self.start_time = time.time()
25
+ self.total_interviews = total_interviews
26
+ self.completed_count = 0
27
+ self.completed_by_model = defaultdict(int)
28
+ self.distinct_models = distinct_models
29
+ self.total_exceptions = 0
30
+ self.unfixed_exceptions = 0
31
+
32
+ def add_completed_interview(
33
+ self, model: str, num_exceptions: int = 0, num_unfixed: int = 0
34
+ ):
35
+ self.completed_count += 1
36
+ self.completed_by_model[model] += 1
37
+ self.total_exceptions += num_exceptions
38
+ self.unfixed_exceptions += num_unfixed
39
+
40
+ def get_elapsed_time(self) -> float:
41
+ return time.time() - self.start_time
42
+
43
+ def get_average_time_per_interview(self) -> float:
44
+ return (
45
+ self.get_elapsed_time() / self.completed_count
46
+ if self.completed_count > 0
47
+ else 0
48
+ )
49
+
50
+ def get_throughput(self) -> float:
51
+ elapsed = self.get_elapsed_time()
52
+ return self.completed_count / elapsed if elapsed > 0 else 0
53
+
54
+ def get_estimated_time_remaining(self) -> float:
55
+ if self.completed_count == 0:
56
+ return 0
57
+ avg_time = self.get_average_time_per_interview()
58
+ remaining = self.total_interviews - self.completed_count
59
+ return avg_time * remaining
60
+
61
+
62
+ class JobsRunnerStatusBase(ABC):
63
+ def __init__(
64
+ self,
65
+ jobs_runner: "JobsRunnerAsyncio",
66
+ n: int,
67
+ refresh_rate: float = 1,
68
+ endpoint_url: Optional[str] = "http://localhost:8000",
69
+ job_uuid: Optional[UUID] = None,
70
+ api_key: str = None,
71
+ ):
72
+ self.jobs_runner = jobs_runner
73
+ self.job_uuid = job_uuid
74
+ self.base_url = f"{endpoint_url}"
75
+ self.refresh_rate = refresh_rate
76
+ self.statistics = [
77
+ "elapsed_time",
78
+ "total_interviews_requested",
79
+ "completed_interviews",
80
+ "average_time_per_interview",
81
+ "estimated_time_remaining",
82
+ "exceptions",
83
+ "unfixed_exceptions",
84
+ "throughput",
85
+ ]
86
+ self.num_total_interviews = n * len(self.jobs_runner)
87
+
88
+ self.distinct_models = list(
89
+ set(model.model for model in self.jobs_runner.jobs.models)
90
+ )
91
+
92
+ self.stats_tracker = StatisticsTracker(
93
+ total_interviews=self.num_total_interviews,
94
+ distinct_models=self.distinct_models,
95
+ )
96
+
97
+ self.api_key = api_key or os.getenv("EXPECTED_PARROT_API_KEY")
98
+
99
+ @abstractmethod
100
+ def has_ep_api_key(self):
101
+ """Checks if the user has an Expected Parrot API key."""
102
+ pass
103
+
104
+ def get_status_dict(self) -> Dict[str, Any]:
105
+ """Converts current status into a JSON-serializable dictionary."""
106
+ # Get all statistics
107
+ stats = {}
108
+ for stat_name in self.statistics:
109
+ stat = self._compute_statistic(stat_name)
110
+ name, value = list(stat.items())[0]
111
+ stats[name] = value
112
+
113
+ # Get model-specific progress
114
+ model_progress = {}
115
+ target_per_model = int(self.num_total_interviews / len(self.distinct_models))
116
+
117
+ for model in self.distinct_models:
118
+ completed = self.stats_tracker.completed_by_model[model]
119
+ model_progress[model] = {
120
+ "completed": completed,
121
+ "total": target_per_model,
122
+ "percent": (
123
+ (completed / target_per_model * 100) if target_per_model > 0 else 0
124
+ ),
125
+ }
126
+
127
+ status_dict = {
128
+ "overall_progress": {
129
+ "completed": self.stats_tracker.completed_count,
130
+ "total": self.num_total_interviews,
131
+ "percent": (
132
+ (
133
+ self.stats_tracker.completed_count
134
+ / self.num_total_interviews
135
+ * 100
136
+ )
137
+ if self.num_total_interviews > 0
138
+ else 0
139
+ ),
140
+ },
141
+ "language_model_progress": model_progress,
142
+ "statistics": stats,
143
+ "status": (
144
+ "completed"
145
+ if self.stats_tracker.completed_count >= self.num_total_interviews
146
+ else "running"
147
+ ),
148
+ }
149
+
150
+ model_queues = {}
151
+ # for model, bucket in self.jobs_runner.bucket_collection.items():
152
+ for model, bucket in self.jobs_runner.environment.bucket_collection.items():
153
+ model_name = model.model
154
+ model_queues[model_name] = {
155
+ "language_model_name": model_name,
156
+ "requests_bucket": {
157
+ "completed": bucket.requests_bucket.num_released,
158
+ "requested": bucket.requests_bucket.num_requests,
159
+ "tokens_returned": bucket.requests_bucket.tokens_returned,
160
+ "target_rate": round(bucket.requests_bucket.target_rate, 1),
161
+ "current_rate": round(bucket.requests_bucket.get_throughput(), 1),
162
+ },
163
+ "tokens_bucket": {
164
+ "completed": bucket.tokens_bucket.num_released,
165
+ "requested": bucket.tokens_bucket.num_requests,
166
+ "tokens_returned": bucket.tokens_bucket.tokens_returned,
167
+ "target_rate": round(bucket.tokens_bucket.target_rate, 1),
168
+ "current_rate": round(bucket.tokens_bucket.get_throughput(), 1),
169
+ },
170
+ }
171
+ status_dict["language_model_queues"] = model_queues
172
+ return status_dict
173
+
174
+ def add_completed_interview(self, result):
175
+ """Records a completed interview without storing the full interview data."""
176
+ self.stats_tracker.add_completed_interview(
177
+ model=result.model.model,
178
+ num_exceptions=(
179
+ len(result.exceptions) if hasattr(result, "exceptions") else 0
180
+ ),
181
+ num_unfixed=(
182
+ result.exceptions.num_unfixed() if hasattr(result, "exceptions") else 0
183
+ ),
184
+ )
185
+
186
+ def _compute_statistic(self, stat_name: str):
187
+ """Computes individual statistics based on the stats tracker."""
188
+ if stat_name == "elapsed_time":
189
+ value = self.stats_tracker.get_elapsed_time()
190
+ return {"elapsed_time": (value, 1, "sec.")}
191
+
192
+ elif stat_name == "total_interviews_requested":
193
+ return {"total_interviews_requested": (self.num_total_interviews, None, "")}
194
+
195
+ elif stat_name == "completed_interviews":
196
+ return {
197
+ "completed_interviews": (self.stats_tracker.completed_count, None, "")
198
+ }
199
+
200
+ elif stat_name == "average_time_per_interview":
201
+ value = self.stats_tracker.get_average_time_per_interview()
202
+ return {"average_time_per_interview": (value, 2, "sec.")}
203
+
204
+ elif stat_name == "estimated_time_remaining":
205
+ value = self.stats_tracker.get_estimated_time_remaining()
206
+ return {"estimated_time_remaining": (value, 1, "sec.")}
207
+
208
+ elif stat_name == "exceptions":
209
+ return {"exceptions": (self.stats_tracker.total_exceptions, None, "")}
210
+
211
+ elif stat_name == "unfixed_exceptions":
212
+ return {
213
+ "unfixed_exceptions": (self.stats_tracker.unfixed_exceptions, None, "")
214
+ }
215
+
216
+ elif stat_name == "throughput":
217
+ value = self.stats_tracker.get_throughput()
218
+ return {"throughput": (value, 2, "interviews/sec.")}
219
+
220
+ def update_progress(self, stop_event):
221
+ while not stop_event.is_set():
222
+ self.send_status_update()
223
+ time.sleep(self.refresh_rate)
224
+ self.send_status_update()
225
+
226
+ @abstractmethod
227
+ def setup(self):
228
+ """Conducts any setup needed prior to sending status updates."""
229
+ pass
230
+
231
+ @abstractmethod
232
+ def send_status_update(self):
233
+ """Updates the current status of the job."""
234
+ pass
235
+
236
+
237
+ class JobsRunnerStatus(JobsRunnerStatusBase):
238
+ @property
239
+ def create_url(self) -> str:
240
+ return f"{self.base_url}/api/v0/local-job"
241
+
242
+ @property
243
+ def viewing_url(self) -> str:
244
+ return f"{self.base_url}/home/local-job-progress/{str(self.job_uuid)}"
245
+
246
+ @property
247
+ def update_url(self) -> str:
248
+ return f"{self.base_url}/api/v0/local-job/{str(self.job_uuid)}"
249
+
250
+ def setup(self) -> None:
251
+ """Creates a local job on Coop if one does not already exist."""
252
+ headers = {
253
+ "Content-Type": "application/json",
254
+ "Authorization": f"Bearer {self.api_key or 'None'}",
255
+ }
256
+
257
+ if self.job_uuid is None:
258
+ response = requests.post(
259
+ self.create_url,
260
+ headers=headers,
261
+ timeout=1,
262
+ )
263
+ response.raise_for_status()
264
+ data = response.json()
265
+ self.job_uuid = data.get("job_uuid")
266
+
267
+ print(f"Running with progress bar. View progress at {self.viewing_url}")
268
+
269
+ def send_status_update(self) -> None:
270
+ """Sends current status to the web endpoint using the instance's job_uuid."""
271
+ try:
272
+ status_dict = self.get_status_dict()
273
+ status_dict["job_id"] = str(self.job_uuid)
274
+
275
+ headers = {
276
+ "Content-Type": "application/json",
277
+ "Authorization": f"Bearer {self.api_key or 'None'}",
278
+ }
279
+
280
+ response = requests.patch(
281
+ self.update_url,
282
+ json=status_dict,
283
+ headers=headers,
284
+ timeout=1,
285
+ )
286
+ response.raise_for_status()
287
+ except requests.exceptions.RequestException as e:
288
+ print(f"Failed to send status update for job {self.job_uuid}: {e}")
289
+
290
+ def has_ep_api_key(self) -> bool:
291
+ """Returns True if the user has an Expected Parrot API key."""
292
+ return self.api_key is not None
293
+
294
+
295
+ if __name__ == "__main__":
296
+ import doctest
297
+
298
+ doctest.testmod(optionflags=doctest.ELLIPSIS)