edsl 0.1.46__py3-none-any.whl → 0.1.48__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (328) hide show
  1. edsl/__init__.py +44 -39
  2. edsl/__version__.py +1 -1
  3. edsl/agents/__init__.py +4 -2
  4. edsl/agents/{Agent.py → agent.py} +442 -152
  5. edsl/agents/{AgentList.py → agent_list.py} +220 -162
  6. edsl/agents/descriptors.py +46 -7
  7. edsl/{exceptions/agents.py → agents/exceptions.py} +3 -12
  8. edsl/base/__init__.py +75 -0
  9. edsl/base/base_class.py +1303 -0
  10. edsl/base/data_transfer_models.py +114 -0
  11. edsl/base/enums.py +215 -0
  12. edsl/base.py +8 -0
  13. edsl/buckets/__init__.py +25 -0
  14. edsl/buckets/bucket_collection.py +324 -0
  15. edsl/buckets/model_buckets.py +206 -0
  16. edsl/buckets/token_bucket.py +502 -0
  17. edsl/{jobs/buckets/TokenBucketAPI.py → buckets/token_bucket_api.py} +1 -1
  18. edsl/buckets/token_bucket_client.py +509 -0
  19. edsl/caching/__init__.py +20 -0
  20. edsl/caching/cache.py +814 -0
  21. edsl/caching/cache_entry.py +427 -0
  22. edsl/{data/CacheHandler.py → caching/cache_handler.py} +14 -15
  23. edsl/caching/exceptions.py +24 -0
  24. edsl/caching/orm.py +30 -0
  25. edsl/{data/RemoteCacheSync.py → caching/remote_cache_sync.py} +3 -3
  26. edsl/caching/sql_dict.py +441 -0
  27. edsl/config/__init__.py +8 -0
  28. edsl/config/config_class.py +177 -0
  29. edsl/config.py +4 -176
  30. edsl/conversation/Conversation.py +7 -7
  31. edsl/conversation/car_buying.py +4 -4
  32. edsl/conversation/chips.py +6 -6
  33. edsl/coop/__init__.py +25 -2
  34. edsl/coop/coop.py +430 -113
  35. edsl/coop/{ExpectedParrotKeyHandler.py → ep_key_handling.py} +86 -10
  36. edsl/coop/exceptions.py +62 -0
  37. edsl/coop/price_fetcher.py +126 -0
  38. edsl/coop/utils.py +89 -24
  39. edsl/data_transfer_models.py +5 -72
  40. edsl/dataset/__init__.py +10 -0
  41. edsl/{results/Dataset.py → dataset/dataset.py} +116 -36
  42. edsl/dataset/dataset_operations_mixin.py +1492 -0
  43. edsl/{results/DatasetTree.py → dataset/dataset_tree.py} +156 -75
  44. edsl/{results/TableDisplay.py → dataset/display/table_display.py} +18 -7
  45. edsl/{results → dataset/display}/table_renderers.py +58 -2
  46. edsl/{results → dataset}/file_exports.py +4 -5
  47. edsl/{results → dataset}/smart_objects.py +2 -2
  48. edsl/enums.py +5 -205
  49. edsl/inference_services/__init__.py +5 -0
  50. edsl/inference_services/{AvailableModelCacheHandler.py → available_model_cache_handler.py} +2 -3
  51. edsl/inference_services/{AvailableModelFetcher.py → available_model_fetcher.py} +8 -14
  52. edsl/inference_services/data_structures.py +3 -2
  53. edsl/{exceptions/inference_services.py → inference_services/exceptions.py} +1 -1
  54. edsl/inference_services/{InferenceServiceABC.py → inference_service_abc.py} +1 -1
  55. edsl/inference_services/{InferenceServicesCollection.py → inference_services_collection.py} +8 -7
  56. edsl/inference_services/registry.py +4 -41
  57. edsl/inference_services/{ServiceAvailability.py → service_availability.py} +5 -25
  58. edsl/inference_services/services/__init__.py +31 -0
  59. edsl/inference_services/{AnthropicService.py → services/anthropic_service.py} +3 -3
  60. edsl/inference_services/{AwsBedrock.py → services/aws_bedrock.py} +2 -2
  61. edsl/inference_services/{AzureAI.py → services/azure_ai.py} +2 -2
  62. edsl/inference_services/{DeepInfraService.py → services/deep_infra_service.py} +1 -3
  63. edsl/inference_services/{DeepSeekService.py → services/deep_seek_service.py} +2 -4
  64. edsl/inference_services/{GoogleService.py → services/google_service.py} +5 -4
  65. edsl/inference_services/{GroqService.py → services/groq_service.py} +1 -1
  66. edsl/inference_services/{MistralAIService.py → services/mistral_ai_service.py} +3 -3
  67. edsl/inference_services/{OllamaService.py → services/ollama_service.py} +1 -7
  68. edsl/inference_services/{OpenAIService.py → services/open_ai_service.py} +5 -6
  69. edsl/inference_services/{PerplexityService.py → services/perplexity_service.py} +12 -12
  70. edsl/inference_services/{TestService.py → services/test_service.py} +7 -6
  71. edsl/inference_services/{TogetherAIService.py → services/together_ai_service.py} +2 -6
  72. edsl/inference_services/{XAIService.py → services/xai_service.py} +1 -1
  73. edsl/inference_services/write_available.py +1 -2
  74. edsl/instructions/__init__.py +6 -0
  75. edsl/{surveys/instructions/Instruction.py → instructions/instruction.py} +11 -6
  76. edsl/{surveys/instructions/InstructionCollection.py → instructions/instruction_collection.py} +10 -5
  77. edsl/{surveys/InstructionHandler.py → instructions/instruction_handler.py} +3 -3
  78. edsl/{jobs/interviews → interviews}/ReportErrors.py +2 -2
  79. edsl/interviews/__init__.py +4 -0
  80. edsl/{jobs/AnswerQuestionFunctionConstructor.py → interviews/answering_function.py} +45 -18
  81. edsl/{jobs/interviews/InterviewExceptionEntry.py → interviews/exception_tracking.py} +107 -22
  82. edsl/interviews/interview.py +638 -0
  83. edsl/{jobs/interviews/InterviewStatusDictionary.py → interviews/interview_status_dictionary.py} +21 -12
  84. edsl/{jobs/interviews/InterviewStatusLog.py → interviews/interview_status_log.py} +16 -7
  85. edsl/{jobs/InterviewTaskManager.py → interviews/interview_task_manager.py} +12 -7
  86. edsl/{jobs/RequestTokenEstimator.py → interviews/request_token_estimator.py} +8 -3
  87. edsl/{jobs/interviews/InterviewStatistic.py → interviews/statistics.py} +36 -10
  88. edsl/invigilators/__init__.py +38 -0
  89. edsl/invigilators/invigilator_base.py +477 -0
  90. edsl/{agents/Invigilator.py → invigilators/invigilators.py} +263 -10
  91. edsl/invigilators/prompt_constructor.py +476 -0
  92. edsl/{agents → invigilators}/prompt_helpers.py +2 -1
  93. edsl/{agents/QuestionInstructionPromptBuilder.py → invigilators/question_instructions_prompt_builder.py} +18 -13
  94. edsl/{agents → invigilators}/question_option_processor.py +96 -21
  95. edsl/{agents/QuestionTemplateReplacementsBuilder.py → invigilators/question_template_replacements_builder.py} +64 -12
  96. edsl/jobs/__init__.py +7 -1
  97. edsl/jobs/async_interview_runner.py +99 -35
  98. edsl/jobs/check_survey_scenario_compatibility.py +7 -5
  99. edsl/jobs/data_structures.py +153 -22
  100. edsl/{exceptions/jobs.py → jobs/exceptions.py} +2 -1
  101. edsl/jobs/{FetchInvigilator.py → fetch_invigilator.py} +4 -4
  102. edsl/jobs/{loggers/HTMLTableJobLogger.py → html_table_job_logger.py} +6 -2
  103. edsl/jobs/{Jobs.py → jobs.py} +321 -155
  104. edsl/jobs/{JobsChecks.py → jobs_checks.py} +15 -7
  105. edsl/jobs/{JobsComponentConstructor.py → jobs_component_constructor.py} +20 -17
  106. edsl/jobs/{InterviewsConstructor.py → jobs_interview_constructor.py} +10 -5
  107. edsl/jobs/jobs_pricing_estimation.py +347 -0
  108. edsl/jobs/{JobsRemoteInferenceLogger.py → jobs_remote_inference_logger.py} +4 -3
  109. edsl/jobs/jobs_runner_asyncio.py +282 -0
  110. edsl/jobs/{JobsRemoteInferenceHandler.py → remote_inference.py} +19 -22
  111. edsl/jobs/results_exceptions_handler.py +2 -2
  112. edsl/key_management/__init__.py +28 -0
  113. edsl/key_management/key_lookup.py +161 -0
  114. edsl/{language_models/key_management/KeyLookupBuilder.py → key_management/key_lookup_builder.py} +118 -47
  115. edsl/key_management/key_lookup_collection.py +82 -0
  116. edsl/key_management/models.py +218 -0
  117. edsl/language_models/__init__.py +7 -2
  118. edsl/language_models/{ComputeCost.py → compute_cost.py} +18 -3
  119. edsl/{exceptions/language_models.py → language_models/exceptions.py} +2 -1
  120. edsl/language_models/language_model.py +1080 -0
  121. edsl/language_models/model.py +10 -25
  122. edsl/language_models/{ModelList.py → model_list.py} +9 -14
  123. edsl/language_models/{RawResponseHandler.py → raw_response_handler.py} +1 -1
  124. edsl/language_models/{RegisterLanguageModelsMeta.py → registry.py} +1 -1
  125. edsl/language_models/repair.py +4 -4
  126. edsl/language_models/utilities.py +4 -4
  127. edsl/notebooks/__init__.py +3 -1
  128. edsl/notebooks/{Notebook.py → notebook.py} +7 -8
  129. edsl/prompts/__init__.py +1 -1
  130. edsl/{exceptions/prompts.py → prompts/exceptions.py} +3 -1
  131. edsl/prompts/{Prompt.py → prompt.py} +101 -95
  132. edsl/questions/HTMLQuestion.py +1 -1
  133. edsl/questions/__init__.py +154 -25
  134. edsl/questions/answer_validator_mixin.py +1 -1
  135. edsl/questions/compose_questions.py +4 -3
  136. edsl/questions/derived/question_likert_five.py +166 -0
  137. edsl/questions/derived/{QuestionLinearScale.py → question_linear_scale.py} +4 -4
  138. edsl/questions/derived/{QuestionTopK.py → question_top_k.py} +4 -4
  139. edsl/questions/derived/{QuestionYesNo.py → question_yes_no.py} +4 -5
  140. edsl/questions/descriptors.py +24 -30
  141. edsl/questions/loop_processor.py +65 -19
  142. edsl/questions/question_base.py +881 -0
  143. edsl/questions/question_base_gen_mixin.py +15 -16
  144. edsl/questions/{QuestionBasePromptsMixin.py → question_base_prompts_mixin.py} +2 -2
  145. edsl/questions/{QuestionBudget.py → question_budget.py} +3 -4
  146. edsl/questions/{QuestionCheckBox.py → question_check_box.py} +16 -16
  147. edsl/questions/{QuestionDict.py → question_dict.py} +39 -5
  148. edsl/questions/{QuestionExtract.py → question_extract.py} +9 -9
  149. edsl/questions/question_free_text.py +282 -0
  150. edsl/questions/{QuestionFunctional.py → question_functional.py} +6 -5
  151. edsl/questions/{QuestionList.py → question_list.py} +6 -7
  152. edsl/questions/{QuestionMatrix.py → question_matrix.py} +6 -5
  153. edsl/questions/{QuestionMultipleChoice.py → question_multiple_choice.py} +126 -21
  154. edsl/questions/{QuestionNumerical.py → question_numerical.py} +5 -5
  155. edsl/questions/{QuestionRank.py → question_rank.py} +6 -6
  156. edsl/questions/question_registry.py +10 -16
  157. edsl/questions/register_questions_meta.py +8 -4
  158. edsl/questions/response_validator_abc.py +17 -16
  159. edsl/results/__init__.py +4 -1
  160. edsl/{exceptions/results.py → results/exceptions.py} +1 -1
  161. edsl/results/report.py +197 -0
  162. edsl/results/{Result.py → result.py} +131 -45
  163. edsl/results/{Results.py → results.py} +420 -216
  164. edsl/results/results_selector.py +344 -25
  165. edsl/scenarios/__init__.py +30 -3
  166. edsl/scenarios/{ConstructDownloadLink.py → construct_download_link.py} +7 -0
  167. edsl/scenarios/directory_scanner.py +156 -13
  168. edsl/scenarios/document_chunker.py +186 -0
  169. edsl/scenarios/exceptions.py +101 -0
  170. edsl/scenarios/file_methods.py +2 -3
  171. edsl/scenarios/file_store.py +755 -0
  172. edsl/scenarios/handlers/__init__.py +14 -14
  173. edsl/scenarios/handlers/{csv.py → csv_file_store.py} +1 -2
  174. edsl/scenarios/handlers/{docx.py → docx_file_store.py} +8 -7
  175. edsl/scenarios/handlers/{html.py → html_file_store.py} +1 -2
  176. edsl/scenarios/handlers/{jpeg.py → jpeg_file_store.py} +1 -1
  177. edsl/scenarios/handlers/{json.py → json_file_store.py} +1 -1
  178. edsl/scenarios/handlers/latex_file_store.py +5 -0
  179. edsl/scenarios/handlers/{md.py → md_file_store.py} +1 -1
  180. edsl/scenarios/handlers/{pdf.py → pdf_file_store.py} +2 -2
  181. edsl/scenarios/handlers/{png.py → png_file_store.py} +1 -1
  182. edsl/scenarios/handlers/{pptx.py → pptx_file_store.py} +8 -7
  183. edsl/scenarios/handlers/{py.py → py_file_store.py} +1 -3
  184. edsl/scenarios/handlers/{sql.py → sql_file_store.py} +2 -1
  185. edsl/scenarios/handlers/{sqlite.py → sqlite_file_store.py} +2 -3
  186. edsl/scenarios/handlers/{txt.py → txt_file_store.py} +1 -1
  187. edsl/scenarios/scenario.py +928 -0
  188. edsl/scenarios/scenario_join.py +18 -5
  189. edsl/scenarios/{ScenarioList.py → scenario_list.py} +424 -106
  190. edsl/scenarios/{ScenarioListPdfMixin.py → scenario_list_pdf_tools.py} +16 -15
  191. edsl/scenarios/scenario_selector.py +5 -1
  192. edsl/study/ObjectEntry.py +2 -2
  193. edsl/study/SnapShot.py +5 -5
  194. edsl/study/Study.py +20 -21
  195. edsl/study/__init__.py +6 -4
  196. edsl/surveys/__init__.py +7 -4
  197. edsl/surveys/dag/__init__.py +2 -0
  198. edsl/surveys/{ConstructDAG.py → dag/construct_dag.py} +3 -3
  199. edsl/surveys/{DAG.py → dag/dag.py} +13 -10
  200. edsl/surveys/descriptors.py +1 -1
  201. edsl/surveys/{EditSurvey.py → edit_survey.py} +9 -9
  202. edsl/{exceptions/surveys.py → surveys/exceptions.py} +1 -2
  203. edsl/surveys/memory/__init__.py +3 -0
  204. edsl/surveys/{MemoryPlan.py → memory/memory_plan.py} +10 -9
  205. edsl/surveys/rules/__init__.py +3 -0
  206. edsl/surveys/{Rule.py → rules/rule.py} +103 -43
  207. edsl/surveys/{RuleCollection.py → rules/rule_collection.py} +21 -30
  208. edsl/surveys/{RuleManager.py → rules/rule_manager.py} +19 -13
  209. edsl/surveys/survey.py +1743 -0
  210. edsl/surveys/{SurveyExportMixin.py → survey_export.py} +22 -27
  211. edsl/surveys/{SurveyFlowVisualization.py → survey_flow_visualization.py} +11 -2
  212. edsl/surveys/{Simulator.py → survey_simulator.py} +10 -3
  213. edsl/tasks/__init__.py +32 -0
  214. edsl/{jobs/tasks/QuestionTaskCreator.py → tasks/question_task_creator.py} +115 -57
  215. edsl/tasks/task_creators.py +135 -0
  216. edsl/{jobs/tasks/TaskHistory.py → tasks/task_history.py} +86 -47
  217. edsl/{jobs/tasks → tasks}/task_status_enum.py +91 -7
  218. edsl/tasks/task_status_log.py +85 -0
  219. edsl/tokens/__init__.py +2 -0
  220. edsl/tokens/interview_token_usage.py +53 -0
  221. edsl/utilities/PrettyList.py +1 -1
  222. edsl/utilities/SystemInfo.py +25 -22
  223. edsl/utilities/__init__.py +29 -21
  224. edsl/utilities/gcp_bucket/__init__.py +2 -0
  225. edsl/utilities/gcp_bucket/cloud_storage.py +99 -96
  226. edsl/utilities/interface.py +44 -536
  227. edsl/{results/MarkdownToPDF.py → utilities/markdown_to_pdf.py} +13 -5
  228. edsl/utilities/repair_functions.py +1 -1
  229. {edsl-0.1.46.dist-info → edsl-0.1.48.dist-info}/METADATA +3 -2
  230. edsl-0.1.48.dist-info/RECORD +347 -0
  231. edsl/Base.py +0 -426
  232. edsl/BaseDiff.py +0 -260
  233. edsl/agents/InvigilatorBase.py +0 -260
  234. edsl/agents/PromptConstructor.py +0 -318
  235. edsl/auto/AutoStudy.py +0 -130
  236. edsl/auto/StageBase.py +0 -243
  237. edsl/auto/StageGenerateSurvey.py +0 -178
  238. edsl/auto/StageLabelQuestions.py +0 -125
  239. edsl/auto/StagePersona.py +0 -61
  240. edsl/auto/StagePersonaDimensionValueRanges.py +0 -88
  241. edsl/auto/StagePersonaDimensionValues.py +0 -74
  242. edsl/auto/StagePersonaDimensions.py +0 -69
  243. edsl/auto/StageQuestions.py +0 -74
  244. edsl/auto/SurveyCreatorPipeline.py +0 -21
  245. edsl/auto/utilities.py +0 -218
  246. edsl/base/Base.py +0 -279
  247. edsl/coop/PriceFetcher.py +0 -54
  248. edsl/data/Cache.py +0 -580
  249. edsl/data/CacheEntry.py +0 -230
  250. edsl/data/SQLiteDict.py +0 -292
  251. edsl/data/__init__.py +0 -5
  252. edsl/data/orm.py +0 -10
  253. edsl/exceptions/cache.py +0 -5
  254. edsl/exceptions/coop.py +0 -14
  255. edsl/exceptions/data.py +0 -14
  256. edsl/exceptions/scenarios.py +0 -29
  257. edsl/jobs/Answers.py +0 -43
  258. edsl/jobs/JobsPrompts.py +0 -354
  259. edsl/jobs/buckets/BucketCollection.py +0 -134
  260. edsl/jobs/buckets/ModelBuckets.py +0 -65
  261. edsl/jobs/buckets/TokenBucket.py +0 -283
  262. edsl/jobs/buckets/TokenBucketClient.py +0 -191
  263. edsl/jobs/interviews/Interview.py +0 -395
  264. edsl/jobs/interviews/InterviewExceptionCollection.py +0 -99
  265. edsl/jobs/interviews/InterviewStatisticsCollection.py +0 -25
  266. edsl/jobs/runners/JobsRunnerAsyncio.py +0 -163
  267. edsl/jobs/runners/JobsRunnerStatusData.py +0 -0
  268. edsl/jobs/tasks/TaskCreators.py +0 -64
  269. edsl/jobs/tasks/TaskStatusLog.py +0 -23
  270. edsl/jobs/tokens/InterviewTokenUsage.py +0 -27
  271. edsl/language_models/LanguageModel.py +0 -635
  272. edsl/language_models/ServiceDataSources.py +0 -0
  273. edsl/language_models/key_management/KeyLookup.py +0 -63
  274. edsl/language_models/key_management/KeyLookupCollection.py +0 -38
  275. edsl/language_models/key_management/models.py +0 -137
  276. edsl/questions/QuestionBase.py +0 -539
  277. edsl/questions/QuestionFreeText.py +0 -130
  278. edsl/questions/derived/QuestionLikertFive.py +0 -76
  279. edsl/results/DatasetExportMixin.py +0 -911
  280. edsl/results/ResultsExportMixin.py +0 -45
  281. edsl/results/TextEditor.py +0 -50
  282. edsl/results/results_fetch_mixin.py +0 -33
  283. edsl/results/results_tools_mixin.py +0 -98
  284. edsl/scenarios/DocumentChunker.py +0 -104
  285. edsl/scenarios/FileStore.py +0 -564
  286. edsl/scenarios/Scenario.py +0 -548
  287. edsl/scenarios/ScenarioHtmlMixin.py +0 -65
  288. edsl/scenarios/ScenarioListExportMixin.py +0 -45
  289. edsl/scenarios/handlers/latex.py +0 -5
  290. edsl/shared.py +0 -1
  291. edsl/surveys/Survey.py +0 -1306
  292. edsl/surveys/SurveyQualtricsImport.py +0 -284
  293. edsl/surveys/SurveyToApp.py +0 -141
  294. edsl/surveys/instructions/__init__.py +0 -0
  295. edsl/tools/__init__.py +0 -1
  296. edsl/tools/clusters.py +0 -192
  297. edsl/tools/embeddings.py +0 -27
  298. edsl/tools/embeddings_plotting.py +0 -118
  299. edsl/tools/plotting.py +0 -112
  300. edsl/tools/summarize.py +0 -18
  301. edsl/utilities/data/Registry.py +0 -6
  302. edsl/utilities/data/__init__.py +0 -1
  303. edsl/utilities/data/scooter_results.json +0 -1
  304. edsl-0.1.46.dist-info/RECORD +0 -366
  305. /edsl/coop/{CoopFunctionsMixin.py → coop_functions.py} +0 -0
  306. /edsl/{results → dataset/display}/CSSParameterizer.py +0 -0
  307. /edsl/{language_models/key_management → dataset/display}/__init__.py +0 -0
  308. /edsl/{results → dataset/display}/table_data_class.py +0 -0
  309. /edsl/{results → dataset/display}/table_display.css +0 -0
  310. /edsl/{results/ResultsGGMixin.py → dataset/r/ggplot.py} +0 -0
  311. /edsl/{results → dataset}/tree_explore.py +0 -0
  312. /edsl/{surveys/instructions/ChangeInstruction.py → instructions/change_instruction.py} +0 -0
  313. /edsl/{jobs/interviews → interviews}/interview_status_enum.py +0 -0
  314. /edsl/jobs/{runners/JobsRunnerStatus.py → jobs_runner_status.py} +0 -0
  315. /edsl/language_models/{PriceManager.py → price_manager.py} +0 -0
  316. /edsl/language_models/{fake_openai_call.py → unused/fake_openai_call.py} +0 -0
  317. /edsl/language_models/{fake_openai_service.py → unused/fake_openai_service.py} +0 -0
  318. /edsl/notebooks/{NotebookToLaTeX.py → notebook_to_latex.py} +0 -0
  319. /edsl/{exceptions/questions.py → questions/exceptions.py} +0 -0
  320. /edsl/questions/{SimpleAskMixin.py → simple_ask_mixin.py} +0 -0
  321. /edsl/surveys/{Memory.py → memory/memory.py} +0 -0
  322. /edsl/surveys/{MemoryManagement.py → memory/memory_management.py} +0 -0
  323. /edsl/surveys/{SurveyCSS.py → survey_css.py} +0 -0
  324. /edsl/{jobs/tokens/TokenUsage.py → tokens/token_usage.py} +0 -0
  325. /edsl/{results/MarkdownToDocx.py → utilities/markdown_to_docx.py} +0 -0
  326. /edsl/{TemplateLoader.py → utilities/template_loader.py} +0 -0
  327. {edsl-0.1.46.dist-info → edsl-0.1.48.dist-info}/LICENSE +0 -0
  328. {edsl-0.1.46.dist-info → edsl-0.1.48.dist-info}/WHEEL +0 -0
edsl/coop/coop.py CHANGED
@@ -2,22 +2,24 @@ import aiohttp
2
2
  import json
3
3
  import requests
4
4
 
5
- from typing import Any, Optional, Union, Literal, TypedDict
5
+ from typing import Any, Optional, Union, Literal, TypedDict, TYPE_CHECKING
6
6
  from uuid import UUID
7
7
 
8
- import edsl
8
+ from .. import __version__
9
9
 
10
- from edsl.config import CONFIG
11
- from edsl.data.CacheEntry import CacheEntry
12
- from edsl.jobs.Jobs import Jobs
13
- from edsl.surveys.Survey import Survey
10
+ from ..config import CONFIG
11
+ from ..caching import CacheEntry
14
12
 
15
- from edsl.exceptions.coop import (
13
+ if TYPE_CHECKING:
14
+ from ..jobs import Jobs
15
+ from ..surveys import Survey
16
+
17
+ from .exceptions import (
16
18
  CoopInvalidURLError,
17
19
  CoopNoUUIDError,
18
20
  CoopServerResponseError,
19
21
  )
20
- from edsl.coop.utils import (
22
+ from .utils import (
21
23
  EDSLObject,
22
24
  ObjectRegistry,
23
25
  ObjectType,
@@ -25,10 +27,10 @@ from edsl.coop.utils import (
25
27
  VisibilityType,
26
28
  )
27
29
 
28
- from edsl.coop.CoopFunctionsMixin import CoopFunctionsMixin
29
- from edsl.coop.ExpectedParrotKeyHandler import ExpectedParrotKeyHandler
30
+ from .coop_functions import CoopFunctionsMixin
31
+ from .ep_key_handling import ExpectedParrotKeyHandler
30
32
 
31
- from edsl.inference_services.data_structures import ServiceToModelsMapping
33
+ from ..inference_services.data_structures import ServiceToModelsMapping
32
34
 
33
35
 
34
36
  class RemoteInferenceResponse(TypedDict):
@@ -54,16 +56,58 @@ class RemoteInferenceCreationInfo(TypedDict):
54
56
 
55
57
  class Coop(CoopFunctionsMixin):
56
58
  """
57
- Client for the Expected Parrot API.
59
+ Client for the Expected Parrot API that provides cloud-based functionality for EDSL.
60
+
61
+ The Coop class is the main interface for interacting with Expected Parrot's cloud services.
62
+ It enables:
63
+
64
+ 1. Storing and retrieving EDSL objects (surveys, agents, models, results, etc.)
65
+ 2. Running inference jobs remotely for better performance and scalability
66
+ 3. Retrieving and caching interview results
67
+ 4. Managing API keys and authentication
68
+ 5. Accessing model availability and pricing information
69
+
70
+ The client handles authentication, serialization/deserialization of EDSL objects,
71
+ and communication with the Expected Parrot API endpoints. It also provides
72
+ methods for tracking job status and managing results.
73
+
74
+ When initialized without parameters, Coop will attempt to use an API key from:
75
+ 1. The EXPECTED_PARROT_API_KEY environment variable
76
+ 2. A stored key in the user's config directory
77
+ 3. Interactive login if needed
78
+
79
+ Attributes:
80
+ api_key (str): The API key used for authentication
81
+ url (str): The base URL for the Expected Parrot API
82
+ api_url (str): The URL for API endpoints (derived from base URL)
58
83
  """
59
84
 
60
85
  def __init__(
61
86
  self, api_key: Optional[str] = None, url: Optional[str] = None
62
87
  ) -> None:
63
88
  """
64
- Initialize the client.
65
- - Provide an API key directly, or through an env variable.
66
- - Provide a URL directly, or use the default one.
89
+ Initialize the Expected Parrot API client.
90
+
91
+ This constructor sets up the connection to Expected Parrot's cloud services.
92
+ If not provided explicitly, it will attempt to obtain an API key from
93
+ environment variables or from a stored location in the user's config directory.
94
+
95
+ Parameters:
96
+ api_key (str, optional): API key for authentication with Expected Parrot.
97
+ If not provided, will attempt to obtain from environment or stored location.
98
+ url (str, optional): Base URL for the Expected Parrot service.
99
+ If not provided, uses the default from configuration.
100
+
101
+ Notes:
102
+ - The API key is stored in the EXPECTED_PARROT_API_KEY environment variable
103
+ or in a platform-specific config directory
104
+ - The URL is determined based on whether it's a production, staging,
105
+ or development environment
106
+ - The api_url for actual API endpoints is derived from the base URL
107
+
108
+ Example:
109
+ >>> coop = Coop() # Uses API key from environment or stored location
110
+ >>> coop = Coop(api_key="your-api-key") # Explicitly provide API key
67
111
  """
68
112
  self.ep_key_handler = ExpectedParrotKeyHandler()
69
113
  self.api_key = api_key or self.ep_key_handler.get_ep_api_key()
@@ -79,7 +123,7 @@ class Coop(CoopFunctionsMixin):
79
123
  self.api_url = "http://localhost:8000"
80
124
  else:
81
125
  self.api_url = self.url
82
- self._edsl_version = edsl.__version__
126
+ self._edsl_version = __version__
83
127
 
84
128
  def get_progress_bar_url(self):
85
129
  return f"{CONFIG.EXPECTED_PARROT_URL}"
@@ -205,7 +249,7 @@ class Coop(CoopFunctionsMixin):
205
249
  # print(response.text)
206
250
  if "The API key you provided is invalid" in message and check_api_key:
207
251
  import secrets
208
- from edsl.utilities.utilities import write_api_key_to_env
252
+ from ..utilities.utilities import write_api_key_to_env
209
253
 
210
254
  edsl_auth_token = secrets.token_urlsafe(16)
211
255
 
@@ -239,6 +283,30 @@ class Coop(CoopFunctionsMixin):
239
283
 
240
284
  raise CoopServerResponseError(message)
241
285
 
286
+ def _resolve_gcs_response(self, response: requests.Response) -> None:
287
+ """
288
+ Check the response from uploading or downloading a file from Google Cloud Storage.
289
+ Raise errors as appropriate.
290
+ """
291
+ if response.status_code >= 400:
292
+ try:
293
+ import xml.etree.ElementTree as ET
294
+
295
+ # Extract elements from XML string
296
+ root = ET.fromstring(response.text)
297
+
298
+ code = root.find("Code").text
299
+ message = root.find("Message").text
300
+ details = root.find("Details").text
301
+ except Exception:
302
+ raise Exception(
303
+ f"Server returned status code {response.status_code}",
304
+ "XML response could not be decoded.",
305
+ "The server response was: " + response.text,
306
+ )
307
+
308
+ raise Exception(f"An error occurred: {code} - {message} - {details}")
309
+
242
310
  def _poll_for_api_key(
243
311
  self, edsl_auth_token: str, timeout: int = 120
244
312
  ) -> Union[str, None]:
@@ -287,19 +355,25 @@ class Coop(CoopFunctionsMixin):
287
355
  if value is None:
288
356
  return "null"
289
357
 
358
+ @staticmethod
359
+ def _is_url(url_or_uuid: Union[str, UUID]) -> bool:
360
+ return "http://" in str(url_or_uuid) or "https://" in str(url_or_uuid)
361
+
290
362
  def _resolve_uuid_or_alias(
291
- self, uuid: Union[str, UUID] = None, url: str = None
363
+ self, url_or_uuid: Union[str, UUID]
292
364
  ) -> tuple[Optional[str], Optional[str], Optional[str]]:
293
365
  """
294
366
  Resolve the uuid or alias information from a uuid or a url.
295
367
  Returns a tuple of (uuid, owner_username, alias)
296
- - For content/<uuid> URLs: returns (uuid, None, None)
297
- - For content/<username>/<alias> URLs: returns (None, username, alias)
368
+ - For content/uuid URLs: returns (uuid, None, None)
369
+ - For content/username/alias URLs: returns (None, username, alias)
298
370
  """
299
- if not url and not uuid:
371
+ if not url_or_uuid:
300
372
  raise CoopNoUUIDError("No uuid or url provided for the object.")
301
373
 
302
- if not uuid and url:
374
+ if self._is_url(url_or_uuid):
375
+ url = str(url_or_uuid)
376
+
303
377
  parts = (
304
378
  url.replace("http://", "")
305
379
  .replace("https://", "")
@@ -326,7 +400,8 @@ class Coop(CoopFunctionsMixin):
326
400
  f"Invalid URL format. The URL must end with /content/<uuid> or /content/<username>/<alias>: {url}"
327
401
  )
328
402
 
329
- return str(uuid), None, None
403
+ uuid = str(url_or_uuid)
404
+ return uuid, None, None
330
405
 
331
406
  @property
332
407
  def edsl_settings(self) -> dict:
@@ -348,6 +423,15 @@ class Coop(CoopFunctionsMixin):
348
423
  ################
349
424
  # Objects
350
425
  ################
426
+ def _get_alias_url(self, owner_username: str, alias: str) -> Union[str, None]:
427
+ """
428
+ Get the URL of an object by its owner username and alias.
429
+ """
430
+ if owner_username and alias:
431
+ return f"{self.url}/content/{owner_username}/{alias}"
432
+ else:
433
+ return None
434
+
351
435
  def create(
352
436
  self,
353
437
  object: EDSLObject,
@@ -356,7 +440,35 @@ class Coop(CoopFunctionsMixin):
356
440
  visibility: Optional[VisibilityType] = "unlisted",
357
441
  ) -> dict:
358
442
  """
359
- Create an EDSL object in the Coop server.
443
+ Store an EDSL object in the Expected Parrot cloud service.
444
+
445
+ This method uploads an EDSL object (like a Survey, Agent, or Results) to the
446
+ Expected Parrot cloud service for storage, sharing, or further processing.
447
+
448
+ Parameters:
449
+ object (EDSLObject): The EDSL object to store (Survey, Agent, Results, etc.)
450
+ description (str, optional): A human-readable description of the object
451
+ alias (str, optional): A custom alias for easier reference later
452
+ visibility (VisibilityType, optional): Access level for the object. One of:
453
+ - "private": Only accessible by the owner
454
+ - "public": Accessible by anyone
455
+ - "unlisted": Accessible with the link, but not listed publicly
456
+
457
+ Returns:
458
+ dict: Information about the created object including:
459
+ - url: The URL to access the object
460
+ - alias_url: The URL with the custom alias (if provided)
461
+ - uuid: The unique identifier for the object
462
+ - visibility: The visibility setting
463
+ - version: The EDSL version used to create the object
464
+
465
+ Raises:
466
+ CoopServerResponseError: If there's an error communicating with the server
467
+
468
+ Example:
469
+ >>> survey = Survey(questions=[QuestionFreeText(question_name="name")])
470
+ >>> result = coop.create(survey, description="Basic survey", visibility="public")
471
+ >>> print(result["url"]) # URL to access the survey
360
472
  """
361
473
  object_type = ObjectRegistry.get_object_type_by_edsl_class(object)
362
474
  response = self._send_server_request(
@@ -365,9 +477,13 @@ class Coop(CoopFunctionsMixin):
365
477
  payload={
366
478
  "description": description,
367
479
  "alias": alias,
368
- "json_string": json.dumps(
369
- object.to_dict(),
370
- default=self._json_handle_none,
480
+ "json_string": (
481
+ json.dumps(
482
+ object.to_dict(),
483
+ default=self._json_handle_none,
484
+ )
485
+ if object_type != "scenario"
486
+ else ""
371
487
  ),
372
488
  "object_type": object_type,
373
489
  "visibility": visibility,
@@ -376,33 +492,77 @@ class Coop(CoopFunctionsMixin):
376
492
  )
377
493
  self._resolve_server_response(response)
378
494
  response_json = response.json()
495
+
496
+ if object_type == "scenario":
497
+ json_data = json.dumps(
498
+ object.to_dict(),
499
+ default=self._json_handle_none,
500
+ )
501
+ headers = {"Content-Type": "application/json"}
502
+ if response_json.get("upload_signed_url"):
503
+ signed_url = response_json.get("upload_signed_url")
504
+ else:
505
+ raise Exception("No signed url provided received")
506
+
507
+ response = requests.put(
508
+ signed_url, data=json_data.encode(), headers=headers
509
+ )
510
+ self._resolve_gcs_response(response)
511
+ owner_username = response_json.get("owner_username")
512
+ object_alias = response_json.get("alias")
513
+
379
514
  return {
380
515
  "description": response_json.get("description"),
381
516
  "object_type": object_type,
382
517
  "url": f"{self.url}/content/{response_json.get('uuid')}",
518
+ "alias_url": self._get_alias_url(owner_username, object_alias),
383
519
  "uuid": response_json.get("uuid"),
384
520
  "version": self._edsl_version,
385
521
  "visibility": response_json.get("visibility"),
522
+ "upload_signed_url": response_json.get("upload_signed_url", None),
386
523
  }
387
524
 
388
525
  def get(
389
526
  self,
390
- uuid: Union[str, UUID] = None,
391
- url: str = None,
527
+ url_or_uuid: Union[str, UUID],
392
528
  expected_object_type: Optional[ObjectType] = None,
393
529
  ) -> EDSLObject:
394
530
  """
395
- Retrieve an EDSL object by its uuid/url or by owner username and alias.
396
- - If the object's visibility is private, the user must be the owner.
397
- - Optionally, check if the retrieved object is of a certain type.
531
+ Retrieve an EDSL object from the Expected Parrot cloud service.
532
+
533
+ This method downloads and deserializes an EDSL object from the cloud service
534
+ using either its UUID, URL, or username/alias combination.
398
535
 
399
- :param uuid: the uuid of the object either in str or UUID format.
400
- :param url: the url of the object (can be content/uuid or content/username/alias format).
401
- :param expected_object_type: the expected type of the object.
536
+ Parameters:
537
+ url_or_uuid (Union[str, UUID]): Identifier for the object to retrieve.
538
+ Can be one of:
539
+ - UUID string (e.g., "123e4567-e89b-12d3-a456-426614174000")
540
+ - Full URL (e.g., "https://expectedparrot.com/content/123e4567...")
541
+ - Alias URL (e.g., "https://expectedparrot.com/content/username/my-survey")
542
+ expected_object_type (ObjectType, optional): If provided, validates that the
543
+ retrieved object is of the expected type (e.g., "survey", "agent")
544
+
545
+ Returns:
546
+ EDSLObject: The retrieved object as its original EDSL class instance
547
+ (e.g., Survey, Agent, Results)
402
548
 
403
- :return: the object instance.
549
+ Raises:
550
+ CoopNoUUIDError: If no UUID or URL is provided
551
+ CoopInvalidURLError: If the URL format is invalid
552
+ CoopServerResponseError: If the server returns an error (e.g., not found,
553
+ unauthorized access)
554
+ Exception: If the retrieved object doesn't match the expected type
555
+
556
+ Notes:
557
+ - If the object's visibility is set to "private", you must be the owner to access it
558
+ - For objects stored with an alias, you can use either the UUID or the alias URL
559
+
560
+ Example:
561
+ >>> survey = coop.get("123e4567-e89b-12d3-a456-426614174000")
562
+ >>> survey = coop.get("https://expectedparrot.com/content/username/my-survey")
563
+ >>> survey = coop.get(url, expected_object_type="survey") # Validates the type
404
564
  """
405
- obj_uuid, owner_username, alias = self._resolve_uuid_or_alias(uuid, url)
565
+ obj_uuid, owner_username, alias = self._resolve_uuid_or_alias(url_or_uuid)
406
566
 
407
567
  if obj_uuid:
408
568
  response = self._send_server_request(
@@ -419,6 +579,11 @@ class Coop(CoopFunctionsMixin):
419
579
 
420
580
  self._resolve_server_response(response)
421
581
  json_string = response.json().get("json_string")
582
+ if "load_from:" in json_string[0:12]:
583
+ load_link = json_string.split("load_from:")[1]
584
+ object_data = requests.get(load_link)
585
+ self._resolve_gcs_response(object_data)
586
+ json_string = object_data.text
422
587
  object_type = response.json().get("object_type")
423
588
  if expected_object_type and object_type != expected_object_type:
424
589
  raise Exception(f"Expected {expected_object_type=} but got {object_type=}")
@@ -437,28 +602,52 @@ class Coop(CoopFunctionsMixin):
437
602
  params={"type": object_type},
438
603
  )
439
604
  self._resolve_server_response(response)
440
- objects = [
441
- {
442
- "object": edsl_class.from_dict(json.loads(o.get("json_string"))),
605
+ objects = []
606
+ for o in response.json():
607
+ json_string = o.get("json_string")
608
+ ## check if load from bucket needed.
609
+ if "load_from:" in json_string[0:12]:
610
+ load_link = json_string.split("load_from:")[1]
611
+ object_data = requests.get(load_link)
612
+ self._resolve_gcs_response(object_data)
613
+ json_string = object_data.text
614
+
615
+ json_string = json.loads(json_string)
616
+ object = {
617
+ "object": edsl_class.from_dict(json_string),
443
618
  "uuid": o.get("uuid"),
444
619
  "version": o.get("version"),
445
620
  "description": o.get("description"),
446
621
  "visibility": o.get("visibility"),
447
622
  "url": f"{self.url}/content/{o.get('uuid')}",
623
+ "alias_url": self._get_alias_url(
624
+ o.get("owner_username"), o.get("alias")
625
+ ),
448
626
  }
449
- for o in response.json()
450
- ]
627
+ objects.append(object)
628
+
451
629
  return objects
452
630
 
453
- def delete(self, uuid: Union[str, UUID] = None, url: str = None) -> dict:
631
+ def delete(self, url_or_uuid: Union[str, UUID]) -> dict:
454
632
  """
455
633
  Delete an object from the server.
634
+
635
+ :param url_or_uuid: The UUID or URL of the object.
636
+ URLs can be in the form content/uuid or content/username/alias.
456
637
  """
457
- obj_uuid, _, _ = self._resolve_uuid_or_alias(uuid, url)
638
+ obj_uuid, owner_username, alias = self._resolve_uuid_or_alias(url_or_uuid)
639
+
640
+ if obj_uuid:
641
+ uri = "api/v0/object"
642
+ params = {"uuid": obj_uuid}
643
+ else:
644
+ uri = "api/v0/object/alias"
645
+ params = {"owner_username": owner_username, "alias": alias}
646
+
458
647
  response = self._send_server_request(
459
- uri=f"api/v0/object",
648
+ uri=uri,
460
649
  method="DELETE",
461
- params={"uuid": obj_uuid},
650
+ params=params,
462
651
  )
463
652
 
464
653
  self._resolve_server_response(response)
@@ -466,8 +655,7 @@ class Coop(CoopFunctionsMixin):
466
655
 
467
656
  def patch(
468
657
  self,
469
- uuid: Union[str, UUID] = None,
470
- url: str = None,
658
+ url_or_uuid: Union[str, UUID],
471
659
  description: Optional[str] = None,
472
660
  alias: Optional[str] = None,
473
661
  value: Optional[EDSLObject] = None,
@@ -475,15 +663,35 @@ class Coop(CoopFunctionsMixin):
475
663
  ) -> dict:
476
664
  """
477
665
  Change the attributes of an uploaded object
478
- - Only supports visibility for now
479
- """
480
- if description is None and visibility is None and value is None:
666
+
667
+ :param url_or_uuid: The UUID or URL of the object.
668
+ URLs can be in the form content/uuid or content/username/alias.
669
+ :param description: Optional new description
670
+ :param alias: Optional new alias
671
+ :param value: Optional new object value
672
+ :param visibility: Optional new visibility setting
673
+ """
674
+ if (
675
+ description is None
676
+ and visibility is None
677
+ and value is None
678
+ and alias is None
679
+ ):
481
680
  raise Exception("Nothing to patch.")
482
- obj_uuid, _, _ = self._resolve_uuid_or_alias(uuid, url)
681
+
682
+ obj_uuid, owner_username, obj_alias = self._resolve_uuid_or_alias(url_or_uuid)
683
+
684
+ if obj_uuid:
685
+ uri = "api/v0/object"
686
+ params = {"uuid": obj_uuid}
687
+ else:
688
+ uri = "api/v0/object/alias"
689
+ params = {"owner_username": owner_username, "alias": obj_alias}
690
+
483
691
  response = self._send_server_request(
484
- uri=f"api/v0/object",
692
+ uri=uri,
485
693
  method="PATCH",
486
- params={"uuid": obj_uuid},
694
+ params=params,
487
695
  payload={
488
696
  "description": description,
489
697
  "alias": alias,
@@ -765,7 +973,7 @@ class Coop(CoopFunctionsMixin):
765
973
 
766
974
  def remote_inference_create(
767
975
  self,
768
- job: Jobs,
976
+ job: "Jobs",
769
977
  description: Optional[str] = None,
770
978
  status: RemoteJobStatus = "queued",
771
979
  visibility: Optional[VisibilityType] = "unlisted",
@@ -774,17 +982,48 @@ class Coop(CoopFunctionsMixin):
774
982
  fresh: Optional[bool] = False,
775
983
  ) -> RemoteInferenceCreationInfo:
776
984
  """
777
- Send a remote inference job to the server.
985
+ Create a remote inference job for execution in the Expected Parrot cloud.
986
+
987
+ This method sends a job to be executed in the cloud, which can be more efficient
988
+ for large jobs or when you want to run jobs in the background. The job execution
989
+ is handled by Expected Parrot's infrastructure, and you can check the status
990
+ and retrieve results later.
991
+
992
+ Parameters:
993
+ job (Jobs): The EDSL job to run in the cloud
994
+ description (str, optional): A human-readable description of the job
995
+ status (RemoteJobStatus): Initial status, should be "queued" for normal use
996
+ Possible values: "queued", "running", "completed", "failed"
997
+ visibility (VisibilityType): Access level for the job information. One of:
998
+ - "private": Only accessible by the owner
999
+ - "public": Accessible by anyone
1000
+ - "unlisted": Accessible with the link, but not listed publicly
1001
+ initial_results_visibility (VisibilityType): Access level for the job results
1002
+ iterations (int): Number of times to run each interview (default: 1)
1003
+ fresh (bool): If True, ignore existing cache entries and generate new results
778
1004
 
779
- :param job: The EDSL job to send to the server.
780
- :param optional description: A description for this entry in the remote cache.
781
- :param status: The status of the job. Should be 'queued', unless you are debugging.
782
- :param visibility: The visibility of the cache entry.
783
- :param iterations: The number of times to run each interview.
784
-
785
- >>> job = Jobs.example()
786
- >>> coop.remote_inference_create(job=job, description="My job")
787
- {'uuid': '9f8484ee-b407-40e4-9652-4133a7236c9c', 'description': 'My job', 'status': 'queued', 'iterations': None, 'visibility': 'unlisted', 'version': '0.1.38.dev1'}
1005
+ Returns:
1006
+ RemoteInferenceCreationInfo: Information about the created job including:
1007
+ - uuid: The unique identifier for the job
1008
+ - description: The job description
1009
+ - status: Current status of the job
1010
+ - iterations: Number of iterations for each interview
1011
+ - visibility: Access level for the job
1012
+ - version: EDSL version used to create the job
1013
+
1014
+ Raises:
1015
+ CoopServerResponseError: If there's an error communicating with the server
1016
+
1017
+ Notes:
1018
+ - Remote jobs run asynchronously and may take time to complete
1019
+ - Use remote_inference_get() with the returned UUID to check status
1020
+ - Credits are consumed based on the complexity of the job
1021
+
1022
+ Example:
1023
+ >>> from edsl.jobs import Jobs
1024
+ >>> job = Jobs.example()
1025
+ >>> job_info = coop.remote_inference_create(job=job, description="My job")
1026
+ >>> print(f"Job created with UUID: {job_info['uuid']}")
788
1027
  """
789
1028
  response = self._send_server_request(
790
1029
  uri="api/v0/remote-inference",
@@ -821,15 +1060,43 @@ class Coop(CoopFunctionsMixin):
821
1060
  self, job_uuid: Optional[str] = None, results_uuid: Optional[str] = None
822
1061
  ) -> RemoteInferenceResponse:
823
1062
  """
824
- Get the details of a remote inference job.
825
- You can pass either the job uuid or the results uuid as a parameter.
826
- If you pass both, the job uuid will be prioritized.
1063
+ Get the status and details of a remote inference job.
1064
+
1065
+ This method retrieves the current status and information about a remote job,
1066
+ including links to results if the job has completed successfully.
827
1067
 
828
- :param job_uuid: The UUID of the EDSL job.
829
- :param results_uuid: The UUID of the results associated with the EDSL job.
1068
+ Parameters:
1069
+ job_uuid (str, optional): The UUID of the remote job to check
1070
+ results_uuid (str, optional): The UUID of the results associated with the job
1071
+ (can be used if you only have the results UUID)
830
1072
 
831
- >>> coop.remote_inference_get("9f8484ee-b407-40e4-9652-4133a7236c9c")
832
- {'job_uuid': '9f8484ee-b407-40e4-9652-4133a7236c9c', 'results_uuid': 'dd708234-31bf-4fe1-8747-6e232625e026', 'results_url': 'https://www.expectedparrot.com/content/dd708234-31bf-4fe1-8747-6e232625e026', 'latest_error_report_uuid': None, 'latest_error_report_url': None, 'status': 'completed', 'reason': None, 'credits_consumed': 0.35, 'version': '0.1.38.dev1'}
1073
+ Returns:
1074
+ RemoteInferenceResponse: Information about the job including:
1075
+ - job_uuid: The unique identifier for the job
1076
+ - results_uuid: The UUID of the results (if job is completed)
1077
+ - results_url: URL to access the results (if available)
1078
+ - latest_error_report_uuid: UUID of error report (if job failed)
1079
+ - latest_error_report_url: URL to access error details (if available)
1080
+ - status: Current status ("queued", "running", "completed", "failed")
1081
+ - reason: Reason for failure (if applicable)
1082
+ - credits_consumed: Credits used for the job execution
1083
+ - version: EDSL version used for the job
1084
+
1085
+ Raises:
1086
+ ValueError: If neither job_uuid nor results_uuid is provided
1087
+ CoopServerResponseError: If there's an error communicating with the server
1088
+
1089
+ Notes:
1090
+ - Either job_uuid or results_uuid must be provided
1091
+ - If both are provided, job_uuid takes precedence
1092
+ - For completed jobs, you can use the results_url to view or download results
1093
+ - For failed jobs, check the latest_error_report_url for debugging information
1094
+
1095
+ Example:
1096
+ >>> job_status = coop.remote_inference_get("9f8484ee-b407-40e4-9652-4133a7236c9c")
1097
+ >>> print(f"Job status: {job_status['status']}")
1098
+ >>> if job_status['status'] == 'completed':
1099
+ ... print(f"Results available at: {job_status['results_url']}")
833
1100
  """
834
1101
  if job_uuid is None and results_uuid is None:
835
1102
  raise ValueError("Either job_uuid or results_uuid must be provided.")
@@ -887,7 +1154,7 @@ class Coop(CoopFunctionsMixin):
887
1154
  return response.json().get("running_jobs", [])
888
1155
 
889
1156
  def remote_inference_cost(
890
- self, input: Union[Jobs, Survey], iterations: int = 1
1157
+ self, input: Union["Jobs", "Survey"], iterations: int = 1
891
1158
  ) -> int:
892
1159
  """
893
1160
  Get the cost of a remote inference job.
@@ -898,6 +1165,9 @@ class Coop(CoopFunctionsMixin):
898
1165
  >>> coop.remote_inference_cost(input=job)
899
1166
  {'credits': 0.77, 'usd': 0.0076950000000000005}
900
1167
  """
1168
+ from ..jobs import Jobs
1169
+ from ..surveys import Survey
1170
+
901
1171
  if isinstance(input, Jobs):
902
1172
  job = input
903
1173
  elif isinstance(input, Survey):
@@ -928,7 +1198,7 @@ class Coop(CoopFunctionsMixin):
928
1198
  ################
929
1199
  def create_project(
930
1200
  self,
931
- survey: Survey,
1201
+ survey: "Survey",
932
1202
  project_name: str = "Project",
933
1203
  survey_description: Optional[str] = None,
934
1204
  survey_alias: Optional[str] = None,
@@ -958,16 +1228,10 @@ class Coop(CoopFunctionsMixin):
958
1228
  "respondent_url": f"{self.url}/respond/{response_json.get('uuid')}",
959
1229
  }
960
1230
 
961
- ################
962
- # DUNDER METHODS
963
- ################
964
1231
  def __repr__(self):
965
1232
  """Return a string representation of the client."""
966
1233
  return f"Client(api_key='{self.api_key}', url='{self.url}')"
967
1234
 
968
- ################
969
- # EXPERIMENTAL
970
- ################
971
1235
  async def remote_async_execute_model_call(
972
1236
  self, model_dict: dict, user_prompt: str, system_prompt: str
973
1237
  ) -> dict:
@@ -1004,12 +1268,39 @@ class Coop(CoopFunctionsMixin):
1004
1268
 
1005
1269
  def fetch_prices(self) -> dict:
1006
1270
  """
1007
- Fetch model prices from Coop. If the request fails, return an empty dict.
1008
- """
1271
+ Fetch the current pricing information for language models.
1009
1272
 
1010
- from edsl.coop.PriceFetcher import PriceFetcher
1273
+ This method retrieves the latest pricing information for all supported language models
1274
+ from the Expected Parrot API. The pricing data is used to estimate costs for jobs
1275
+ and to optimize model selection based on budget constraints.
1011
1276
 
1012
- from edsl.config import CONFIG
1277
+ Returns:
1278
+ dict: A dictionary mapping (service, model) tuples to pricing information.
1279
+ Each entry contains token pricing for input and output tokens.
1280
+ Example structure:
1281
+ {
1282
+ ('openai', 'gpt-4'): {
1283
+ 'input': {'usd_per_1M_tokens': 30.0, ...},
1284
+ 'output': {'usd_per_1M_tokens': 60.0, ...}
1285
+ }
1286
+ }
1287
+
1288
+ Raises:
1289
+ ValueError: If the EDSL_FETCH_TOKEN_PRICES configuration setting is invalid
1290
+
1291
+ Notes:
1292
+ - Returns an empty dict if EDSL_FETCH_TOKEN_PRICES is set to "False"
1293
+ - The pricing data is cached to minimize API calls
1294
+ - Pricing may vary based on the model, provider, and token type (input/output)
1295
+ - All prices are in USD per million tokens
1296
+
1297
+ Example:
1298
+ >>> prices = coop.fetch_prices()
1299
+ >>> gpt4_price = prices.get(('openai', 'gpt-4'), {})
1300
+ >>> print(f"GPT-4 input price: ${gpt4_price.get('input', {}).get('usd_per_1M_tokens')}")
1301
+ """
1302
+ from .price_fetcher import PriceFetcher
1303
+ from ..config import CONFIG
1013
1304
 
1014
1305
  if CONFIG.get("EDSL_FETCH_TOKEN_PRICES") == "True":
1015
1306
  price_fetcher = PriceFetcher()
@@ -1023,9 +1314,37 @@ class Coop(CoopFunctionsMixin):
1023
1314
 
1024
1315
  def fetch_models(self) -> ServiceToModelsMapping:
1025
1316
  """
1026
- Fetch a dict of available models from Coop.
1317
+ Fetch information about available language models from Expected Parrot.
1027
1318
 
1028
- Each key in the dict is an inference service, and each value is a list of models from that service.
1319
+ This method retrieves the current list of available language models grouped
1320
+ by service provider (e.g., OpenAI, Anthropic, etc.). This information is
1321
+ useful for programmatically selecting models based on availability and
1322
+ for ensuring that jobs only use supported models.
1323
+
1324
+ Returns:
1325
+ ServiceToModelsMapping: A mapping of service providers to their available models.
1326
+ Example structure:
1327
+ {
1328
+ "openai": ["gpt-4", "gpt-3.5-turbo", ...],
1329
+ "anthropic": ["claude-3-opus", "claude-3-sonnet", ...],
1330
+ ...
1331
+ }
1332
+
1333
+ Raises:
1334
+ CoopServerResponseError: If there's an error communicating with the server
1335
+
1336
+ Notes:
1337
+ - The availability of models may change over time
1338
+ - Not all models may be accessible with your current API keys
1339
+ - Use this method to check for model availability before creating jobs
1340
+ - Models may have different capabilities (text-only, multimodal, etc.)
1341
+
1342
+ Example:
1343
+ >>> models = coop.fetch_models()
1344
+ >>> if "gpt-4" in models.get("openai", []):
1345
+ ... print("GPT-4 is available")
1346
+ >>> available_services = list(models.keys())
1347
+ >>> print(f"Available services: {available_services}")
1029
1348
  """
1030
1349
  response = self._send_server_request(uri="api/v0/models", method="GET")
1031
1350
  self._resolve_server_response(response)
@@ -1134,7 +1453,7 @@ class Coop(CoopFunctionsMixin):
1134
1453
  """
1135
1454
  import secrets
1136
1455
  from dotenv import load_dotenv
1137
- from edsl.utilities.utilities import write_api_key_to_env
1456
+ from ..utilities.utilities import write_api_key_to_env
1138
1457
 
1139
1458
  edsl_auth_token = secrets.token_urlsafe(16)
1140
1459
 
@@ -1172,9 +1491,9 @@ def main():
1172
1491
  ScenarioList,
1173
1492
  Survey,
1174
1493
  )
1175
- from edsl.coop import Coop
1176
- from edsl.data.CacheEntry import CacheEntry
1177
- from edsl.jobs import Jobs
1494
+ from ..coop import Coop
1495
+ from ..caching import CacheEntry
1496
+ from ..jobs import Jobs
1178
1497
 
1179
1498
  # init & basics
1180
1499
  API_KEY = "b"
@@ -1187,27 +1506,25 @@ def main():
1187
1506
  ##############
1188
1507
  # .. create and manipulate an object through the Coop client
1189
1508
  response = coop.create(QuestionMultipleChoice.example())
1190
- coop.get(uuid=response.get("uuid"))
1191
- coop.get(uuid=response.get("uuid"), expected_object_type="question")
1192
- coop.get(url=response.get("url"))
1509
+ coop.get(response.get("uuid"))
1510
+ coop.get(response.get("uuid"), expected_object_type="question")
1511
+ coop.get(response.get("url"))
1193
1512
  coop.create(QuestionMultipleChoice.example())
1194
1513
  coop.get_all("question")
1195
- coop.patch(uuid=response.get("uuid"), visibility="private")
1196
- coop.patch(uuid=response.get("uuid"), description="hey")
1197
- coop.patch(uuid=response.get("uuid"), value=QuestionFreeText.example())
1198
- # coop.patch(uuid=response.get("uuid"), value=Survey.example()) - should throw error
1199
- coop.get(uuid=response.get("uuid"))
1200
- coop.delete(uuid=response.get("uuid"))
1514
+ coop.patch(response.get("uuid"), visibility="private")
1515
+ coop.patch(response.get("uuid"), description="hey")
1516
+ coop.patch(response.get("uuid"), value=QuestionFreeText.example())
1517
+ # coop.patch(response.get("uuid"), value=Survey.example()) - should throw error
1518
+ coop.get(response.get("uuid"))
1519
+ coop.delete(response.get("uuid"))
1201
1520
 
1202
1521
  # .. create and manipulate an object through the class
1203
1522
  response = QuestionMultipleChoice.example().push()
1204
- QuestionMultipleChoice.pull(uuid=response.get("uuid"))
1205
- QuestionMultipleChoice.pull(url=response.get("url"))
1206
- QuestionMultipleChoice.patch(uuid=response.get("uuid"), visibility="private")
1207
- QuestionMultipleChoice.patch(uuid=response.get("uuid"), description="hey")
1208
- QuestionMultipleChoice.patch(
1209
- uuid=response.get("uuid"), value=QuestionFreeText.example()
1210
- )
1523
+ QuestionMultipleChoice.pull(response.get("uuid"))
1524
+ QuestionMultipleChoice.pull(response.get("url"))
1525
+ QuestionMultipleChoice.patch(response.get("uuid"), visibility="private")
1526
+ QuestionMultipleChoice.patch(response.get("uuid"), description="hey")
1527
+ QuestionMultipleChoice.patch(response.get("uuid"), value=QuestionFreeText.example())
1211
1528
  QuestionMultipleChoice.pull(response.get("uuid"))
1212
1529
  QuestionMultipleChoice.delete(response.get("uuid"))
1213
1530
 
@@ -1230,7 +1547,7 @@ def main():
1230
1547
  # 1. Delete existing objects
1231
1548
  existing_objects = coop.get_all(object_type)
1232
1549
  for item in existing_objects:
1233
- coop.delete(uuid=item.get("uuid"))
1550
+ coop.delete(item.get("uuid"))
1234
1551
  # 2. Create new objects
1235
1552
  example = cls.example()
1236
1553
  response_1 = coop.create(example)
@@ -1244,21 +1561,21 @@ def main():
1244
1561
  assert len(objects) == 4
1245
1562
  # 4. Try to retrieve an item that does not exist
1246
1563
  try:
1247
- coop.get(uuid=uuid4())
1564
+ coop.get(uuid4())
1248
1565
  except Exception as e:
1249
1566
  print(e)
1250
1567
  # 5. Try to retrieve all test objects by their uuids
1251
1568
  for response in [response_1, response_2, response_3, response_4]:
1252
- coop.get(uuid=response.get("uuid"))
1569
+ coop.get(response.get("uuid"))
1253
1570
  # 6. Change visibility of all objects
1254
1571
  for item in objects:
1255
- coop.patch(uuid=item.get("uuid"), visibility="private")
1572
+ coop.patch(item.get("uuid"), visibility="private")
1256
1573
  # 6. Change description of all objects
1257
1574
  for item in objects:
1258
- coop.patch(uuid=item.get("uuid"), description="hey")
1575
+ coop.patch(item.get("uuid"), description="hey")
1259
1576
  # 7. Delete all objects
1260
1577
  for item in objects:
1261
- coop.delete(uuid=item.get("uuid"))
1578
+ coop.delete(item.get("uuid"))
1262
1579
  assert len(coop.get_all(object_type)) == 0
1263
1580
 
1264
1581
  ##############
@@ -1291,4 +1608,4 @@ def main():
1291
1608
  coop.remote_inference_cost(job)
1292
1609
  job_coop_object = coop.remote_inference_create(job)
1293
1610
  job_coop_results = coop.remote_inference_get(job_coop_object.get("uuid"))
1294
- coop.get(uuid=job_coop_results.get("results_uuid"))
1611
+ coop.get(job_coop_results.get("results_uuid"))