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
@@ -2,6 +2,52 @@ from jinja2 import Environment, meta
2
2
  from typing import List, Optional, Union
3
3
 
4
4
 
5
+ def extract_template_variables(ast) -> List[Union[str, tuple]]:
6
+ """
7
+ Extract variable expressions from a Jinja2 AST.
8
+
9
+ Args:
10
+ ast: Jinja2 AST
11
+
12
+ Returns:
13
+ List[Union[str, tuple]]: List of variable names or tuples for dotted paths
14
+ """
15
+ from jinja2 import nodes
16
+ from jinja2.visitor import NodeVisitor
17
+
18
+ variables = []
19
+
20
+ class VariableVisitor(NodeVisitor):
21
+ def visit_Name(self, node):
22
+ variables.append(node.name)
23
+
24
+ def visit_Getattr(self, node):
25
+ # For dotted access like scenario.question_options
26
+ parts = []
27
+ current = node
28
+
29
+ # Handle the leaf attribute
30
+ parts.append(node.attr)
31
+
32
+ # Walk up the chain to collect all parts
33
+ while isinstance(current.node, nodes.Getattr):
34
+ current = current.node
35
+ parts.append(current.attr)
36
+
37
+ # Add the root name
38
+ if isinstance(current.node, nodes.Name):
39
+ parts.append(current.node.name)
40
+
41
+ # Reverse to get the correct order
42
+ parts.reverse()
43
+ variables.append(tuple(parts))
44
+
45
+ visitor = VariableVisitor()
46
+ visitor.visit(ast)
47
+
48
+ return variables
49
+
50
+
5
51
  class QuestionOptionProcessor:
6
52
  """
7
53
  Class that manages the processing of question options.
@@ -16,7 +62,11 @@ class QuestionOptionProcessor:
16
62
  return cls(scenario, prior_answers_dict)
17
63
 
18
64
  def __init__(self, scenario: 'Scenario', prior_answers_dict: dict):
19
- self.scenario = scenario
65
+ # This handles cases where the question has {{ scenario.key }} - eventually
66
+ # we might not allow 'naked' scenario keys w/o the scenario prefix
67
+ #new_scenario = scenario.copy()
68
+ #new_scenario.update({'scenario': new_scenario})
69
+ self.scenario = scenario
20
70
  self.prior_answers_dict = prior_answers_dict
21
71
 
22
72
  @staticmethod
@@ -25,18 +75,23 @@ class QuestionOptionProcessor:
25
75
  return [f"<< Option {i} - Placeholder >>" for i in range(1, 4)]
26
76
 
27
77
  @staticmethod
28
- def _parse_template_variable(template_str: str) -> str:
78
+ def _parse_template_variable(template_str: str) -> Union[str, tuple]:
29
79
  """
30
80
  Extract the variable name from a template string.
81
+ If the variable contains dots (e.g., scenario.question_options),
82
+ returns a tuple of the path components.
31
83
 
32
84
  Args:
33
85
  template_str (str): Jinja template string
34
86
 
35
87
  Returns:
36
- str: Name of the first undefined variable in the template
88
+ Union[str, tuple]: Name of the first undefined variable in the template,
89
+ or a tuple of path components if the variable contains dots
37
90
 
38
91
  >>> QuestionOptionProcessor._parse_template_variable("Here are some {{ options }}")
39
92
  'options'
93
+ >>> QuestionOptionProcessor._parse_template_variable("Here are some {{ scenario.question_options }}")
94
+ ('scenario', 'question_options')
40
95
  >>> QuestionOptionProcessor._parse_template_variable("Here are some {{ options }} and {{ other }}")
41
96
  Traceback (most recent call last):
42
97
  ...
@@ -48,11 +103,13 @@ class QuestionOptionProcessor:
48
103
  """
49
104
  env = Environment()
50
105
  parsed_content = env.parse(template_str)
51
- undeclared_variables = list(meta.find_undeclared_variables(parsed_content))
106
+ undeclared_variables = extract_template_variables(parsed_content)
107
+
52
108
  if not undeclared_variables:
53
109
  raise ValueError("No variables found in template string")
54
110
  if len(undeclared_variables) > 1:
55
111
  raise ValueError("Multiple variables found in template string")
112
+
56
113
  return undeclared_variables[0]
57
114
 
58
115
  @staticmethod
@@ -128,7 +185,7 @@ class QuestionOptionProcessor:
128
185
 
129
186
  The case where options are provided as a template string:
130
187
 
131
- >>> question_data = {"question_options": "{{ options }}"}
188
+ >>> question_data = {"question_options": "{{ scenario.options }}"}
132
189
  >>> processor.get_question_options(question_data)
133
190
  ['Option 1', 'Option 2']
134
191
 
@@ -140,7 +197,7 @@ class QuestionOptionProcessor:
140
197
  >>> q0.answer = ["Option 1", "Option 2"]
141
198
  >>> mpc.prior_answers_dict = lambda: {'q0': q0}
142
199
  >>> processor = QuestionOptionProcessor.from_prompt_constructor(mpc)
143
- >>> question_data = {"question_options": "{{ q0 }}"}
200
+ >>> question_data = {"question_options": "{{ q0.answer }}"}
144
201
  >>> processor.get_question_options(question_data)
145
202
  ['Option 1', 'Option 2']
146
203
 
@@ -156,21 +213,39 @@ class QuestionOptionProcessor:
156
213
  return options_entry if options_entry else self._get_default_options()
157
214
 
158
215
  # Parse template to get variable name
159
- option_key = self._parse_template_variable(options_entry)
160
-
161
- # Try getting options from scenario
162
- scenario_options = self._get_options_from_scenario(
163
- self.scenario, option_key
164
- )
165
- if scenario_options:
166
- return scenario_options
167
-
168
- # Try getting options from prior answers
169
- prior_answer_options = self._get_options_from_prior_answers(
170
- self.prior_answers_dict, option_key
171
- )
172
- if prior_answer_options:
173
- return prior_answer_options
216
+ raw_option_key = self._parse_template_variable(options_entry)
217
+
218
+ source_type = None
219
+
220
+ if isinstance(raw_option_key, tuple):
221
+ if raw_option_key[0] == 'scenario':
222
+ source_type = 'scenario'
223
+ option_key = raw_option_key[-1]
224
+ else:
225
+ source_type = 'prior_answers'
226
+ option_key = raw_option_key[0]
227
+ #breakpoint()
228
+ else:
229
+ option_key = raw_option_key
230
+
231
+ #breakpoint()
232
+
233
+ if source_type == 'scenario':
234
+ # Try getting options from scenario
235
+ scenario_options = self._get_options_from_scenario(
236
+ self.scenario, option_key
237
+ )
238
+ if scenario_options:
239
+ return scenario_options
240
+
241
+ if source_type == 'prior_answers':
242
+
243
+ # Try getting options from prior answers
244
+ prior_answer_options = self._get_options_from_prior_answers(
245
+ self.prior_answers_dict, option_key
246
+ )
247
+ if prior_answer_options:
248
+ return prior_answer_options
174
249
 
175
250
  return self._get_default_options()
176
251
 
@@ -1,11 +1,12 @@
1
1
  from jinja2 import Environment, meta, TemplateSyntaxError
2
2
  from typing import Any, Set, TYPE_CHECKING
3
3
 
4
+ from ..agents import Agent
5
+ from ..scenarios import Scenario
6
+
4
7
  if TYPE_CHECKING:
5
- from edsl.agents.PromptConstructor import PromptConstructor
6
- from edsl.scenarios.Scenario import Scenario
7
- from edsl.questions.QuestionBase import QuestionBase
8
- from edsl.agents.Agent import Agent
8
+ from .prompt_constructor import PromptConstructor
9
+ from ..questions import QuestionBase
9
10
 
10
11
 
11
12
  class QuestionTemplateReplacementsBuilder:
@@ -33,17 +34,29 @@ class QuestionTemplateReplacementsBuilder:
33
34
 
34
35
  def question_file_keys(self):
35
36
  """
36
- >>> from edsl import QuestionMultipleChoice, Scenario
37
+ >>> from ..questions import QuestionMultipleChoice
38
+ >>> from ..scenarios import Scenario
37
39
  >>> q = QuestionMultipleChoice(question_text="Do you like school?", question_name = "q0", question_options = ["yes", "no"])
38
40
  >>> qtrb = QuestionTemplateReplacementsBuilder(scenario = {"file1": "file1"}, question = q, prior_answers_dict = {'q0': 'q0'}, agent = "agent")
39
41
  >>> qtrb.question_file_keys()
40
42
  []
41
- >>> from edsl import FileStore
43
+ >>> from ..scenarios import FileStore
42
44
  >>> fs = FileStore.example()
45
+ >>> # Test direct key reference
43
46
  >>> q = QuestionMultipleChoice(question_text="What do you think of this file: {{ file1 }}", question_name = "q0", question_options = ["good", "bad"])
44
47
  >>> qtrb = QuestionTemplateReplacementsBuilder(scenario = Scenario({"file1": fs}), question = q, prior_answers_dict = {'q0': 'q0'}, agent = "agent")
45
48
  >>> qtrb.question_file_keys()
46
49
  ['file1']
50
+ >>> # Test scenario.key reference
51
+ >>> q = QuestionMultipleChoice(question_text="What do you think of this file: {{ scenario.file2 }}", question_name = "q0", question_options = ["good", "bad"])
52
+ >>> qtrb = QuestionTemplateReplacementsBuilder(scenario = Scenario({"file2": fs}), question = q, prior_answers_dict = {'q0': 'q0'}, agent = "agent")
53
+ >>> qtrb.question_file_keys()
54
+ ['file2']
55
+ >>> # Test both formats in the same question
56
+ >>> q = QuestionMultipleChoice(question_text="Compare {{ file1 }} with {{ scenario.file2 }}", question_name = "q0", question_options = ["good", "bad"])
57
+ >>> qtrb = QuestionTemplateReplacementsBuilder(scenario = Scenario({"file1": fs, "file2": fs}), question = q, prior_answers_dict = {'q0': 'q0'}, agent = "agent")
58
+ >>> sorted(qtrb.question_file_keys())
59
+ ['file1', 'file2']
47
60
  """
48
61
  question_text = self.question.question_text
49
62
  file_keys = self._find_file_keys(self.scenario)
@@ -76,8 +89,8 @@ class QuestionTemplateReplacementsBuilder:
76
89
  """We need to find all the keys in the scenario that refer to FileStore objects.
77
90
  These will be used to append to the prompt a list of files that are part of the scenario.
78
91
 
79
- >>> from edsl import Scenario
80
- >>> from edsl.scenarios.FileStore import FileStore
92
+ >>> from ..scenarios import Scenario
93
+ >>> from ..scenarios import FileStore
81
94
  >>> import tempfile
82
95
  >>> with tempfile.NamedTemporaryFile() as f:
83
96
  ... _ = f.write(b"Hello, world!")
@@ -87,7 +100,7 @@ class QuestionTemplateReplacementsBuilder:
87
100
  ... QuestionTemplateReplacementsBuilder._find_file_keys(scenario)
88
101
  ['fs_file']
89
102
  """
90
- from edsl.scenarios.FileStore import FileStore
103
+ from ..scenarios import FileStore
91
104
 
92
105
  file_entries = []
93
106
  for key, value in scenario.items():
@@ -100,10 +113,11 @@ class QuestionTemplateReplacementsBuilder:
100
113
  question_text: str, scenario_file_keys: list
101
114
  ) -> list:
102
115
  """
103
- Extracts the file keys from a question text.
116
+ Extracts the file keys from a question text, handling both direct references ({{ file_key }})
117
+ and scenario-prefixed references ({{ scenario.file_key }}).
104
118
 
105
119
  >>> from edsl import Scenario
106
- >>> from edsl.scenarios.FileStore import FileStore
120
+ >>> from edsl.scenarios import FileStore
107
121
  >>> import tempfile
108
122
  >>> with tempfile.NamedTemporaryFile() as f:
109
123
  ... _ = f.write(b"Hello, world!")
@@ -112,15 +126,53 @@ class QuestionTemplateReplacementsBuilder:
112
126
  ... scenario = Scenario({"fs_file": fs, 'a': 1})
113
127
  ... QuestionTemplateReplacementsBuilder._extract_file_keys_from_question_text("{{ fs_file }}", ['fs_file'])
114
128
  ['fs_file']
129
+ >>> with tempfile.NamedTemporaryFile() as f:
130
+ ... _ = f.write(b"Hello, world!")
131
+ ... _ = f.seek(0)
132
+ ... fs = FileStore(f.name)
133
+ ... scenario = Scenario({"print": fs, 'a': 1})
134
+ ... QuestionTemplateReplacementsBuilder._extract_file_keys_from_question_text("{{ scenario.print }}", ['print'])
135
+ ['print']
136
+ >>> with tempfile.NamedTemporaryFile() as f:
137
+ ... _ = f.write(b"Hello, world!")
138
+ ... _ = f.seek(0)
139
+ ... fs = FileStore(f.name)
140
+ ... scenario = Scenario({"file1": fs, "file2": fs})
141
+ ... sorted(QuestionTemplateReplacementsBuilder._extract_file_keys_from_question_text("Compare {{ file1 }} with {{ scenario.file2 }}", ['file1', 'file2']))
142
+ ['file1', 'file2']
115
143
  """
116
144
  variables = QuestionTemplateReplacementsBuilder.get_jinja2_variables(
117
145
  question_text
118
146
  )
119
147
  question_file_keys = []
148
+
149
+ # Direct references: {{ file_key }}
120
150
  for var in variables:
121
151
  if var in scenario_file_keys:
122
152
  question_file_keys.append(var)
123
- return question_file_keys
153
+
154
+ # Scenario-prefixed references: {{ scenario.file_key }}
155
+ for var in variables:
156
+ if var == "scenario":
157
+ # If we find a scenario variable, we need to check for nested references
158
+ # Create a modified template with just {{ scenario.* }} expressions to isolate them
159
+ scenario_template = "".join([
160
+ "{% for key, value in scenario.items() %}{{ key }}{% endfor %}"
161
+ ])
162
+ try:
163
+ # This is a check to make sure there's scenario.something syntax in the template
164
+ if "scenario." in question_text:
165
+ # Extract dot-notation scenario references by parsing the template
166
+ import re
167
+ scenario_refs = re.findall(r'{{\s*scenario\.(\w+)\s*}}', question_text)
168
+ for key in scenario_refs:
169
+ if key in scenario_file_keys:
170
+ question_file_keys.append(key)
171
+ except:
172
+ # If there's any issue parsing, just continue with what we have
173
+ pass
174
+
175
+ return list(set(question_file_keys)) # Remove duplicates
124
176
 
125
177
  def _scenario_replacements(
126
178
  self, replacement_string: str = "<see file {key}>"
edsl/jobs/__init__.py CHANGED
@@ -1 +1,7 @@
1
- from edsl.jobs.Jobs import Jobs
1
+ from .jobs import Jobs
2
+ from .jobs import RunConfig, RunParameters, RunEnvironment
3
+ from .remote_inference import JobsRemoteInferenceHandler
4
+ from .jobs_runner_status import JobsRunnerStatusBase
5
+
6
+
7
+ __all__ = ["Jobs"]
@@ -1,43 +1,94 @@
1
+ """
2
+ Asynchronous interview runner module for conducting interviews concurrently.
3
+
4
+ This module provides functionality to run multiple interviews in parallel
5
+ with controlled concurrency, supporting both error handling and result collection.
6
+ """
7
+
1
8
  from collections.abc import AsyncGenerator
2
- from typing import List, TypeVar, Generator, Tuple, TYPE_CHECKING
9
+ from typing import List, Generator, Tuple, TYPE_CHECKING
3
10
  from dataclasses import dataclass
4
11
  import asyncio
5
- from contextlib import asynccontextmanager
6
12
  from edsl.data_transfer_models import EDSLResultObjectInput
7
13
 
8
- from edsl.results.Result import Result
9
- from edsl.jobs.interviews.Interview import Interview
10
- from edsl.config import Config
14
+ from ..results import Result
15
+ from ..interviews import Interview
16
+ from ..config import Config
11
17
  config = Config()
12
18
 
13
19
  if TYPE_CHECKING:
14
- from edsl.jobs.Jobs import Jobs
20
+ from ..jobs import Jobs
15
21
 
22
+ from .data_structures import RunConfig
16
23
 
17
24
  @dataclass
18
25
  class InterviewResult:
26
+ """Container for the result of an interview along with metadata.
27
+
28
+ Attributes:
29
+ result: The Result object containing the interview answers
30
+ interview: The Interview object used to conduct the interview
31
+ order: The original position of this interview in the processing queue
32
+ """
19
33
  result: Result
20
34
  interview: Interview
21
35
  order: int
22
36
 
23
37
 
24
- from edsl.jobs.data_structures import RunConfig
25
-
26
-
27
38
  class AsyncInterviewRunner:
39
+ """
40
+ Runs interviews asynchronously with controlled concurrency.
41
+
42
+ This class manages the parallel execution of multiple interviews while
43
+ respecting concurrency limits and handling errors appropriately.
44
+
45
+ Examples:
46
+ >>> from unittest.mock import MagicMock, AsyncMock
47
+ >>> mock_jobs = MagicMock()
48
+ >>> mock_run_config = MagicMock()
49
+ >>> mock_run_config.parameters.n = 1
50
+ >>> mock_run_config.environment.cache = None
51
+ >>> runner = AsyncInterviewRunner(mock_jobs, mock_run_config)
52
+ >>> isinstance(runner._initialized, asyncio.Event)
53
+ True
54
+ """
55
+
28
56
  MAX_CONCURRENT = int(config.EDSL_MAX_CONCURRENT_TASKS)
29
57
 
30
58
  def __init__(self, jobs: "Jobs", run_config: RunConfig):
59
+ """
60
+ Initialize the AsyncInterviewRunner.
61
+
62
+ Args:
63
+ jobs: The Jobs object that generates interviews
64
+ run_config: Configuration for running the interviews
65
+ """
31
66
  self.jobs = jobs
32
67
  self.run_config = run_config
33
68
  self._initialized = asyncio.Event()
34
69
 
35
70
  def _expand_interviews(self) -> Generator["Interview", None, None]:
36
- """Populates self.total_interviews with n copies of each interview.
37
-
38
- It also has to set the cache for each interview.
39
-
40
- :param n: how many times to run each interview.
71
+ """
72
+ Create multiple copies of each interview based on the run configuration.
73
+
74
+ This method expands interviews for repeated runs and ensures each has
75
+ the proper cache configuration.
76
+
77
+ Yields:
78
+ Interview objects ready to be conducted
79
+
80
+ Examples:
81
+ >>> from unittest.mock import MagicMock
82
+ >>> mock_jobs = MagicMock()
83
+ >>> mock_interview = MagicMock()
84
+ >>> mock_jobs.generate_interviews.return_value = [mock_interview]
85
+ >>> mock_run_config = MagicMock()
86
+ >>> mock_run_config.parameters.n = 2
87
+ >>> mock_run_config.environment.cache = "mock_cache"
88
+ >>> runner = AsyncInterviewRunner(mock_jobs, mock_run_config)
89
+ >>> interviews = list(runner._expand_interviews())
90
+ >>> len(interviews)
91
+ 2
41
92
  """
42
93
  for interview in self.jobs.generate_interviews():
43
94
  for iteration in range(self.run_config.parameters.n):
@@ -52,21 +103,22 @@ class AsyncInterviewRunner:
52
103
  async def _conduct_interview(
53
104
  self, interview: "Interview"
54
105
  ) -> Tuple["Result", "Interview"]:
55
- """Conducts an interview and returns the result object, along with the associated interview.
56
-
57
- We return the interview because it is not populated with exceptions, if any.
58
-
59
- :param interview: the interview to conduct
60
- :return: the result of the interview
61
-
62
- 'extracted_answers' is a dictionary of the answers to the questions in the interview.
63
- This is not the same as the generated_tokens---it can include substantial cleaning and processing / validation.
64
106
  """
65
- # the model buckets are used to track usage rates
66
- # model_buckets = self.bucket_collection[interview.model]
67
- # model_buckets = self.run_config.environment.bucket_collection[interview.model]
68
-
69
- # get the results of the interview e.g., {'how_are_you':"Good" 'how_are_you_generated_tokens': "Good"}
107
+ Asynchronously conduct a single interview.
108
+
109
+ This method performs the interview and creates a Result object with
110
+ the extracted answers and model responses.
111
+
112
+ Args:
113
+ interview: The interview to conduct
114
+
115
+ Returns:
116
+ Tuple containing the Result object and the Interview object
117
+
118
+ Notes:
119
+ 'extracted_answers' contains the processed and validated answers
120
+ from the interview, which may differ from the raw model output.
121
+ """
70
122
  extracted_answers: dict[str, str]
71
123
  model_response_objects: List[EDSLResultObjectInput]
72
124
 
@@ -83,10 +135,20 @@ class AsyncInterviewRunner:
83
135
  async def run(
84
136
  self,
85
137
  ) -> AsyncGenerator[tuple[Result, Interview], None]:
86
- """Creates and processes tasks asynchronously, yielding results as they complete.
87
-
88
- Uses TaskGroup for structured concurrency and automated cleanup.
89
- Results are yielded as they become available while maintaining controlled concurrency.
138
+ """
139
+ Run all interviews asynchronously and yield results as they complete.
140
+
141
+ This method processes interviews in chunks based on MAX_CONCURRENT,
142
+ maintaining controlled concurrency while yielding results as soon as
143
+ they become available.
144
+
145
+ Yields:
146
+ Tuples of (Result, Interview) as interviews complete
147
+
148
+ Notes:
149
+ - Uses structured concurrency patterns for proper resource management
150
+ - Handles exceptions according to the run configuration
151
+ - Ensures task cleanup even in case of failures
90
152
  """
91
153
  interviews = list(self._expand_interviews())
92
154
  self._initialized.set()
@@ -102,10 +164,8 @@ class AsyncInterviewRunner:
102
164
  result.order = idx
103
165
  return InterviewResult(result, interview, idx)
104
166
  except Exception as e:
105
- # breakpoint()
106
167
  if self.run_config.parameters.stop_on_exception:
107
168
  raise
108
- # logger.error(f"Task failed with error: {e}")
109
169
  return None
110
170
 
111
171
  # Process interviews in chunks
@@ -130,7 +190,6 @@ class AsyncInterviewRunner:
130
190
  except Exception as e:
131
191
  if self.run_config.parameters.stop_on_exception:
132
192
  raise
133
- # logger.error(f"Chunk processing failed with error: {e}")
134
193
  continue
135
194
 
136
195
  finally:
@@ -138,3 +197,8 @@ class AsyncInterviewRunner:
138
197
  for task in tasks:
139
198
  if not task.done():
140
199
  task.cancel()
200
+
201
+
202
+ if __name__ == "__main__":
203
+ import doctest
204
+ doctest.testmod()
@@ -1,5 +1,7 @@
1
1
  import warnings
2
2
  from typing import TYPE_CHECKING
3
+ from edsl.scenarios import ScenarioList
4
+ from edsl.surveys import Survey
3
5
 
4
6
  if TYPE_CHECKING:
5
7
  from edsl.surveys.Survey import Survey
@@ -15,10 +17,10 @@ class CheckSurveyScenarioCompatibility:
15
17
  def check(self, strict: bool = False, warn: bool = False) -> None:
16
18
  """Check if the parameters in the survey and scenarios are consistent.
17
19
 
18
- >>> from edsl.jobs.Jobs import Jobs
19
- >>> from edsl.questions.QuestionFreeText import QuestionFreeText
20
- >>> from edsl.surveys.Survey import Survey
21
- >>> from edsl.scenarios.Scenario import Scenario
20
+ >>> from edsl.jobs import Jobs
21
+ >>> from edsl.questions import QuestionFreeText
22
+ >>> from edsl.surveys import Survey
23
+ >>> from edsl.scenarios import Scenario
22
24
  >>> q = QuestionFreeText(question_text = "{{poo}}", question_name = "ugly_question")
23
25
  >>> j = Jobs(survey = Survey(questions=[q]))
24
26
  >>> cs = CheckSurveyScenarioCompatibility(j.survey, j.scenarios)
@@ -39,7 +41,7 @@ class CheckSurveyScenarioCompatibility:
39
41
 
40
42
  >>> q = QuestionFreeText(question_text = "Hello", question_name = "ugly_question")
41
43
  >>> s = Scenario({'ugly_question': "B"})
42
- >>> from edsl.scenarios.ScenarioList import ScenarioList
44
+ >>> from edsl.scenarios import ScenarioList
43
45
  >>> cs = CheckSurveyScenarioCompatibility(Survey(questions=[q]), ScenarioList([s]))
44
46
  >>> cs.check()
45
47
  Traceback (most recent call last):