edsl 0.1.47__py3-none-any.whl → 0.1.49__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 (314) 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 +311 -75
  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/{results/DatasetExportMixin.py → dataset/dataset_operations_mixin.py} +606 -122
  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} +3 -7
  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} +313 -167
  104. edsl/jobs/{JobsChecks.py → jobs_checks.py} +15 -7
  105. edsl/jobs/{JobsComponentConstructor.py → jobs_component_constructor.py} +19 -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 +4 -9
  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} +365 -220
  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/{FileStore.py → file_store.py} +275 -189
  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} +294 -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 +18 -19
  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.47.dist-info → edsl-0.1.49.dist-info}/METADATA +1 -1
  230. edsl-0.1.49.dist-info/RECORD +347 -0
  231. edsl/Base.py +0 -493
  232. edsl/BaseDiff.py +0 -260
  233. edsl/agents/InvigilatorBase.py +0 -260
  234. edsl/agents/PromptConstructor.py +0 -318
  235. edsl/coop/PriceFetcher.py +0 -54
  236. edsl/data/Cache.py +0 -582
  237. edsl/data/CacheEntry.py +0 -238
  238. edsl/data/SQLiteDict.py +0 -292
  239. edsl/data/__init__.py +0 -5
  240. edsl/data/orm.py +0 -10
  241. edsl/exceptions/cache.py +0 -5
  242. edsl/exceptions/coop.py +0 -14
  243. edsl/exceptions/data.py +0 -14
  244. edsl/exceptions/scenarios.py +0 -29
  245. edsl/jobs/Answers.py +0 -43
  246. edsl/jobs/JobsPrompts.py +0 -354
  247. edsl/jobs/buckets/BucketCollection.py +0 -134
  248. edsl/jobs/buckets/ModelBuckets.py +0 -65
  249. edsl/jobs/buckets/TokenBucket.py +0 -283
  250. edsl/jobs/buckets/TokenBucketClient.py +0 -191
  251. edsl/jobs/interviews/Interview.py +0 -395
  252. edsl/jobs/interviews/InterviewExceptionCollection.py +0 -99
  253. edsl/jobs/interviews/InterviewStatisticsCollection.py +0 -25
  254. edsl/jobs/runners/JobsRunnerAsyncio.py +0 -163
  255. edsl/jobs/runners/JobsRunnerStatusData.py +0 -0
  256. edsl/jobs/tasks/TaskCreators.py +0 -64
  257. edsl/jobs/tasks/TaskStatusLog.py +0 -23
  258. edsl/jobs/tokens/InterviewTokenUsage.py +0 -27
  259. edsl/language_models/LanguageModel.py +0 -635
  260. edsl/language_models/ServiceDataSources.py +0 -0
  261. edsl/language_models/key_management/KeyLookup.py +0 -63
  262. edsl/language_models/key_management/KeyLookupCollection.py +0 -38
  263. edsl/language_models/key_management/models.py +0 -137
  264. edsl/questions/QuestionBase.py +0 -544
  265. edsl/questions/QuestionFreeText.py +0 -130
  266. edsl/questions/derived/QuestionLikertFive.py +0 -76
  267. edsl/results/ResultsExportMixin.py +0 -45
  268. edsl/results/TextEditor.py +0 -50
  269. edsl/results/results_fetch_mixin.py +0 -33
  270. edsl/results/results_tools_mixin.py +0 -98
  271. edsl/scenarios/DocumentChunker.py +0 -104
  272. edsl/scenarios/Scenario.py +0 -548
  273. edsl/scenarios/ScenarioHtmlMixin.py +0 -65
  274. edsl/scenarios/ScenarioListExportMixin.py +0 -45
  275. edsl/scenarios/handlers/latex.py +0 -5
  276. edsl/shared.py +0 -1
  277. edsl/surveys/Survey.py +0 -1301
  278. edsl/surveys/SurveyQualtricsImport.py +0 -284
  279. edsl/surveys/SurveyToApp.py +0 -141
  280. edsl/surveys/instructions/__init__.py +0 -0
  281. edsl/tools/__init__.py +0 -1
  282. edsl/tools/clusters.py +0 -192
  283. edsl/tools/embeddings.py +0 -27
  284. edsl/tools/embeddings_plotting.py +0 -118
  285. edsl/tools/plotting.py +0 -112
  286. edsl/tools/summarize.py +0 -18
  287. edsl/utilities/data/Registry.py +0 -6
  288. edsl/utilities/data/__init__.py +0 -1
  289. edsl/utilities/data/scooter_results.json +0 -1
  290. edsl-0.1.47.dist-info/RECORD +0 -354
  291. /edsl/coop/{CoopFunctionsMixin.py → coop_functions.py} +0 -0
  292. /edsl/{results → dataset/display}/CSSParameterizer.py +0 -0
  293. /edsl/{language_models/key_management → dataset/display}/__init__.py +0 -0
  294. /edsl/{results → dataset/display}/table_data_class.py +0 -0
  295. /edsl/{results → dataset/display}/table_display.css +0 -0
  296. /edsl/{results/ResultsGGMixin.py → dataset/r/ggplot.py} +0 -0
  297. /edsl/{results → dataset}/tree_explore.py +0 -0
  298. /edsl/{surveys/instructions/ChangeInstruction.py → instructions/change_instruction.py} +0 -0
  299. /edsl/{jobs/interviews → interviews}/interview_status_enum.py +0 -0
  300. /edsl/jobs/{runners/JobsRunnerStatus.py → jobs_runner_status.py} +0 -0
  301. /edsl/language_models/{PriceManager.py → price_manager.py} +0 -0
  302. /edsl/language_models/{fake_openai_call.py → unused/fake_openai_call.py} +0 -0
  303. /edsl/language_models/{fake_openai_service.py → unused/fake_openai_service.py} +0 -0
  304. /edsl/notebooks/{NotebookToLaTeX.py → notebook_to_latex.py} +0 -0
  305. /edsl/{exceptions/questions.py → questions/exceptions.py} +0 -0
  306. /edsl/questions/{SimpleAskMixin.py → simple_ask_mixin.py} +0 -0
  307. /edsl/surveys/{Memory.py → memory/memory.py} +0 -0
  308. /edsl/surveys/{MemoryManagement.py → memory/memory_management.py} +0 -0
  309. /edsl/surveys/{SurveyCSS.py → survey_css.py} +0 -0
  310. /edsl/{jobs/tokens/TokenUsage.py → tokens/token_usage.py} +0 -0
  311. /edsl/{results/MarkdownToDocx.py → utilities/markdown_to_docx.py} +0 -0
  312. /edsl/{TemplateLoader.py → utilities/template_loader.py} +0 -0
  313. {edsl-0.1.47.dist-info → edsl-0.1.49.dist-info}/LICENSE +0 -0
  314. {edsl-0.1.47.dist-info → edsl-0.1.49.dist-info}/WHEEL +0 -0
@@ -20,25 +20,23 @@ with a low (-1) priority.
20
20
  import ast
21
21
  import random
22
22
  from typing import Any, Union, List
23
+ from collections import defaultdict
23
24
 
24
25
 
25
26
  # from rich import print
26
27
  from simpleeval import EvalWithCompoundTypes
27
28
 
28
- from edsl.exceptions.surveys import SurveyError
29
+ from ..exceptions import SurveyError
29
30
 
30
- from edsl.exceptions.surveys import (
31
+ from ..exceptions import (
31
32
  SurveyRuleCannotEvaluateError,
32
- SurveyRuleCollectionHasNoRulesAtNodeError,
33
33
  SurveyRuleRefersToFutureStateError,
34
- SurveyRuleReferenceInRuleToUnknownQuestionError,
35
34
  SurveyRuleSendsYouBackwardsError,
36
35
  SurveyRuleSkipLogicSyntaxError,
37
36
  )
38
- from edsl.surveys.base import EndOfSurvey
39
- from edsl.utilities.ast_utilities import extract_variable_names
40
- from edsl.utilities.remove_edsl_version import remove_edsl_version
41
37
 
38
+ from ..base import EndOfSurvey
39
+ from ...utilities import extract_variable_names, remove_edsl_version
42
40
 
43
41
  class QuestionIndex:
44
42
  def __set_name__(self, owner, name):
@@ -58,19 +56,11 @@ class QuestionIndex:
58
56
 
59
57
 
60
58
  class Rule:
61
- """The Rule class defines a "rule" for determining the next question presented to an agent."""
59
+ """The Rule class defines a "rule" for determining the next question present."""
62
60
 
63
61
  current_q = QuestionIndex()
64
62
  next_q = QuestionIndex()
65
63
 
66
- # Not implemented but nice to have:
67
- # We could potentially use the question pydantic models to check for rule conflicts, as
68
- # they define the potential trees through a survey.
69
-
70
- # We could also use the AST to check for conflicts by inspecting the types of a rule.
71
- # For example, if we know the answer to a question is a string, we could check that
72
- # the expression only contains string comparisons.
73
-
74
64
  def __init__(
75
65
  self,
76
66
  current_q: int,
@@ -114,8 +104,6 @@ class Rule:
114
104
  f"The expression {self.expression} is not valid Python syntax."
115
105
  )
116
106
 
117
- # get the names of the variables in the expression
118
- # e.g., q1 == 'yes' -> ['q1']
119
107
  extracted_question_names = extract_variable_names(self.ast_tree)
120
108
 
121
109
  # make sure all the variables in the expression are known questions
@@ -123,11 +111,6 @@ class Rule:
123
111
  assert all([q in question_name_to_index for q in extracted_question_names])
124
112
  except AssertionError:
125
113
  pass
126
- # import warnings
127
- # warnings.warn(f"There is an extracted field in the expression that is not a known question. It could be a scenario variable. That's fine! But it also could be a typo or mistake.")
128
- # print(f"Question name to index: {question_name_to_index}")
129
- # print(f"Extracted question names: {extracted_question_names}")
130
- # raise SurveyRuleReferenceInRuleToUnknownQuestionError
131
114
 
132
115
  # get the indices of the questions mentioned in the expression
133
116
  self.named_questions_by_index = [
@@ -144,6 +127,16 @@ class Rule:
144
127
  "A rule refers to a future question, the answer to which would not be available here."
145
128
  )
146
129
  raise SurveyRuleRefersToFutureStateError
130
+
131
+ if (referenced_questions := self._prior_question_is_in_expression()) and not self._is_jinja2_expression(): #raise ValueError("This uses the old syntax!")
132
+ import warnings
133
+ old_expression = self.expression
134
+ for q in referenced_questions:
135
+ if q + ".answer" in self.expression:
136
+ self.expression = self.expression.replace(q + ".answer", f"{{{{ {q}.answer }}}}")
137
+ else:
138
+ self.expression = self.expression.replace(q, f"{{{{ {q}.answer }}}}")
139
+ warnings.warn(f"This uses the old syntax! Converting to Jinja2 style with {{ }}.\nOld expression: {old_expression}\nNew expression: {self.expression}")
147
140
 
148
141
  def _checks(self):
149
142
  pass
@@ -153,7 +146,7 @@ class Rule:
153
146
 
154
147
  >>> r = Rule.example()
155
148
  >>> r.to_dict()
156
- {'current_q': 1, 'expression': "q1 == 'yes'", 'next_q': 2, 'priority': 0, 'question_name_to_index': {'q1': 1}, 'before_rule': False}
149
+ {'current_q': 1, 'expression': "{{ q1.answer }} == 'yes'", 'next_q': 2, 'priority': 0, 'question_name_to_index': {'q1': 1}, 'before_rule': False}
157
150
  """
158
151
  return {
159
152
  "current_q": self.current_q,
@@ -225,6 +218,14 @@ class Rule:
225
218
  replacement = str(value)
226
219
  d[var] = replacement
227
220
  return d
221
+
222
+ def _prior_question_is_in_expression(self) -> set:
223
+ """Check if the expression contains a reference to a prior question."""
224
+ return {q for q in self.question_name_to_index.keys() if q in self.expression}
225
+
226
+ def _is_jinja2_expression(self):
227
+ """Check if the expression is a Jinja2 expression."""
228
+ return "{{" in self.expression and "}}" in self.expression
228
229
 
229
230
  def evaluate(self, current_info_env: dict[int, Any]):
230
231
  """Compute the value of the expression, given a dictionary of known questions answers.
@@ -234,27 +235,89 @@ class Rule:
234
235
  If the expression cannot be evaluated, it raises a CannotEvaluate exception.
235
236
 
236
237
  >>> r = Rule.example()
237
- >>> r.evaluate({'q1' : 'yes'})
238
+ >>> r.evaluate({'q1.answer' : 'yes'})
238
239
  True
239
- >>> r.evaluate({'q1' : 'no'})
240
+ >>> r.evaluate({'q1.answer' : 'no'})
240
241
  False
241
242
 
242
243
  >>> r = Rule.example(jinja2=True)
243
- >>> r.evaluate({'q1' : 'yes'})
244
+ >>> r.evaluate({'q1.answer' : 'yes'})
244
245
  True
245
246
 
246
247
  >>> r = Rule.example(jinja2=True)
247
- >>> r.evaluate({'q1' : 'This is q1'})
248
+ >>> r.evaluate({'q1.answer' : 'This is q1'})
248
249
  False
249
250
 
250
- >>> r = Rule.example(jinja2=False, bad = True)
251
- >>> r.evaluate({'q1' : 'yes'})
252
- Traceback (most recent call last):
253
- ...
254
- edsl.exceptions.surveys.SurveyRuleCannotEvaluateError...
251
+ >>> import warnings
252
+ >>> with warnings.catch_warnings(record=True) as w:
253
+ ... expression = "q1 == 'yes'"
254
+ ... r = Rule(current_q=1, expression=expression, next_q=2, question_name_to_index={"q1": 1}, priority=0)
255
+ ... result = r.evaluate({'q1.answer' : 'yes'})
256
+ ... assert len(w) == 1 # Verify warning was issued
257
+ ... assert result == True
255
258
  """
256
259
  from jinja2 import Template
257
260
 
261
+ def jinja_ize_dictionary(dictionary):
262
+ """Convert a dictionary to a Jinja2 dictionary.
263
+
264
+ Keys must be either:
265
+ - 'agent'
266
+ - 'scenario'
267
+ - A valid question name from question_name_to_index
268
+
269
+ For question keys, the value is wrapped in an 'answer' subdictionary.
270
+
271
+ Examples:
272
+ >>> d = jinja_ize_dictionary({'q1': 'yes'}, {'q1': 1})
273
+ >>> d['q1']['answer']
274
+ 'yes'
275
+
276
+ >>> d = jinja_ize_dictionary({'agent': 'friendly'}, {'q1': 1})
277
+ >>> d['agent']
278
+ 'friendly'
279
+ """
280
+ jinja_dict = defaultdict(dict)
281
+
282
+ for key, value in dictionary.items():
283
+ # print("Now processing key: ", key)
284
+ # print(f"key: {key}, value: {value}")
285
+ # Handle special keys
286
+ if 'agent.' in key:
287
+ # print("Agent key found")
288
+ jinja_dict['agent'][key.split('.')[1]] = value
289
+ # print("jinja dict: ", jinja_dict)
290
+ continue
291
+
292
+ if 'scenario.' in key:
293
+ # print("Scenario key found")
294
+ jinja_dict['scenario'][key.split('.')[1]] = value
295
+ # print("jinja dict: ", jinja_dict)
296
+ continue
297
+
298
+ # print("On to question keys")
299
+ for question_name in self.question_name_to_index.keys():
300
+ # print("question_name: ", question_name)
301
+ if question_name in key:
302
+ if question_name == key:
303
+ # print("question name is key; it's an answer")
304
+ jinja_dict[question_name]['answer'] = value
305
+ # print("jinja dict: ", jinja_dict)
306
+ continue
307
+ else:
308
+ # print("question name is not key; it's a sub-type")
309
+ if "." in key:
310
+ passed_name, value_type = key.split('.')
311
+ # print("passed_name: ", passed_name)
312
+ # print("value_type: ", value_type)
313
+ if passed_name == question_name:
314
+ # print("passed name is question name; it's a sub-type")
315
+ jinja_dict[question_name][value_type] = value
316
+ # print("jinja dict: ", jinja_dict)
317
+ continue
318
+
319
+ return jinja_dict
320
+
258
321
  def substitute_in_answers(expression, current_info_env):
259
322
  """Take the dictionary of answers and substitute them into the expression."""
260
323
 
@@ -262,19 +325,16 @@ class Rule:
262
325
 
263
326
  if "{{" in expression and "}}" in expression:
264
327
  template_expression = Template(self.expression)
265
- to_evaluate = template_expression.render(current_info)
328
+ jinja_dict = jinja_ize_dictionary(current_info)
329
+ to_evaluate = template_expression.render(jinja_dict)
266
330
  else:
267
- # import warnings
268
- # import textwrap
269
- # warnings.warn(textwrap.dedent("""\
270
- # The expression is not a Jinja2 template with {{ }}. This is not recommended.
271
- # You can re-write your expression say "q1 == 'yes'" as "{{ q1 }} == 'yes'".
272
- # """))
273
331
  to_evaluate = expression
274
332
  for var, value in current_info.items():
275
333
  to_evaluate = to_evaluate.replace(var, value)
276
334
 
277
335
  return to_evaluate
336
+
337
+ #breakpoint()
278
338
 
279
339
  try:
280
340
  to_evaluate = substitute_in_answers(self.expression, current_info_env)
@@ -300,16 +360,16 @@ class Rule:
300
360
  def example(cls, jinja2=False, bad=False):
301
361
  if jinja2:
302
362
  # a rule written in jinja2 style with {{ }}
303
- expression = "{{ q1 }} == 'yes'"
363
+ expression = "{{ q1.answer }} == 'yes'"
304
364
  else:
305
- expression = "q1 == 'yes'"
365
+ expression = "{{ q1.answer }} == 'yes'"
306
366
 
307
367
  if bad and jinja2:
308
368
  # a rule written in jinja2 style with {{ }} but with a 'bad' expression
309
369
  expression = "{{ q1 }} == 'This is q1'"
310
370
 
311
371
  if bad and not jinja2:
312
- expression = "q1 == 'This is q1'"
372
+ expression = "{{ q1.answer }} == 'This is q1'"
313
373
 
314
374
  r = Rule(
315
375
  current_q=1,
@@ -3,15 +3,14 @@
3
3
  from typing import List, Union, Any, Optional
4
4
  from collections import defaultdict, UserList, namedtuple
5
5
 
6
- from edsl.exceptions.surveys import (
6
+ from ..exceptions import (
7
7
  SurveyRuleCannotEvaluateError,
8
8
  SurveyRuleCollectionHasNoRulesAtNodeError,
9
9
  )
10
10
 
11
- from edsl.surveys.Rule import Rule
12
- from edsl.surveys.base import EndOfSurvey
13
- from edsl.surveys.DAG import DAG
14
-
11
+ from .rule import Rule
12
+ from ..base import EndOfSurvey
13
+ from ..dag import DAG
15
14
 
16
15
  NextQuestion = namedtuple(
17
16
  "NextQuestion", "next_q, num_rules_found, expressions_evaluating_to_true, priority"
@@ -45,7 +44,7 @@ class RuleCollection(UserList):
45
44
 
46
45
  def to_dataset(self):
47
46
  """Return a Dataset object representation of the RuleCollection object."""
48
- from edsl.results.Dataset import Dataset
47
+ from ...dataset import Dataset
49
48
 
50
49
  keys = ["current_q", "expression", "next_q", "priority", "before_rule"]
51
50
  rule_list = {}
@@ -56,13 +55,21 @@ class RuleCollection(UserList):
56
55
  return Dataset([{k: v} for k, v in rule_list.items()])
57
56
 
58
57
  def _repr_html_(self):
59
- """Return an HTML representation of the RuleCollection object."""
60
- from edsl.results.Dataset import Dataset
61
-
58
+ """Return an HTML representation of the RuleCollection object.
59
+
60
+ >>> rule_collection = RuleCollection.example()
61
+ >>> _ = rule_collection._repr_html_()
62
+ """
62
63
  return self.to_dataset()._repr_html_()
63
64
 
64
65
  def to_dict(self, add_edsl_version=True):
65
- """Create a dictionary representation of the RuleCollection object."""
66
+ """Create a dictionary representation of the RuleCollection object.
67
+ >>> rule_collection = RuleCollection.example()
68
+ >>> rule_collection_dict = rule_collection.to_dict()
69
+ >>> new_rule_collection = RuleCollection.from_dict(rule_collection_dict)
70
+ >>> repr(new_rule_collection) == repr(rule_collection)
71
+ True
72
+ """
66
73
  return {
67
74
  "rules": [rule.to_dict() for rule in self],
68
75
  "num_questions": self.num_questions,
@@ -108,18 +115,6 @@ class RuleCollection(UserList):
108
115
 
109
116
  def show_rules(self) -> None:
110
117
  """Print the rules in a table.
111
-
112
-
113
- .. code-block:: python
114
-
115
- rule_collection = RuleCollection.example()
116
- rule_collection.show_rules()
117
- ┏━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━┓
118
- ┃ current_q ┃ expression ┃ next_q ┃ priority ┃ before_rule ┃
119
- ┡━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━┩
120
- │ 1 │ q1 == 'yes' │ 3 │ 1 │ False │
121
- │ 1 │ q1 == 'no' │ 2 │ 1 │ False │
122
- └───────────┴─────────────┴────────┴──────────┴─────────────┘
123
118
  """
124
119
  return self.to_dataset()
125
120
 
@@ -140,7 +135,6 @@ class RuleCollection(UserList):
140
135
  >>> rule_collection.add_rule(r)
141
136
  >>> rule_collection.skip_question_before_running(1, {})
142
137
  False
143
-
144
138
  """
145
139
  for rule in self.applicable_rules(q_now, before_rule=True):
146
140
  if rule.evaluate(answers):
@@ -157,7 +151,7 @@ class RuleCollection(UserList):
157
151
 
158
152
  >>> rule_collection = RuleCollection.example()
159
153
  >>> rule_collection.applicable_rules(1)
160
- [Rule(current_q=1, expression="q1 == 'yes'", next_q=3, priority=1, question_name_to_index={'q1': 1, 'q2': 2, 'q3': 3, 'q4': 4}, before_rule=False), Rule(current_q=1, expression="q1 == 'no'", next_q=2, priority=1, question_name_to_index={'q1': 1, 'q2': 2, 'q3': 3, 'q4': 4}, before_rule=False)]
154
+ [Rule(current_q=1, expression="{{ q1.answer }} == 'yes'", next_q=3, priority=1, question_name_to_index={'q1': 1, 'q2': 2, 'q3': 3, 'q4': 4}, before_rule=False), Rule(current_q=1, expression="{{ q1.answer }} == 'no'", next_q=2, priority=1, question_name_to_index={'q1': 1, 'q2': 2, 'q3': 3, 'q4': 4}, before_rule=False)]
161
155
 
162
156
  The default is that the rule is applied after the question is asked.
163
157
  If we want to see the rules that apply before the question is asked, we can set before_rule=True.
@@ -189,7 +183,7 @@ class RuleCollection(UserList):
189
183
  :param answers: The answers to the survey questions so far, including the current question.
190
184
 
191
185
  >>> rule_collection = RuleCollection.example()
192
- >>> rule_collection.next_question(1, {'q1': 'yes'})
186
+ >>> rule_collection.next_question(1, {'q1.answer': 'yes'})
193
187
  NextQuestion(next_q=3, num_rules_found=2, expressions_evaluating_to_true=1, priority=1)
194
188
 
195
189
  """
@@ -361,14 +355,14 @@ class RuleCollection(UserList):
361
355
  rules=[
362
356
  Rule(
363
357
  current_q=1,
364
- expression="q1 == 'yes'",
358
+ expression="{{ q1.answer }} == 'yes'",
365
359
  next_q=3,
366
360
  priority=1,
367
361
  question_name_to_index=qn2i,
368
362
  ),
369
363
  Rule(
370
364
  current_q=1,
371
- expression="q1 == 'no'",
365
+ expression="{{ q1.answer }} == 'no'",
372
366
  next_q=2,
373
367
  priority=1,
374
368
  question_name_to_index=qn2i,
@@ -379,7 +373,4 @@ class RuleCollection(UserList):
379
373
 
380
374
  if __name__ == "__main__":
381
375
  import doctest
382
-
383
376
  doctest.testmod(optionflags=doctest.ELLIPSIS)
384
-
385
- print(RuleCollection.example()._repr_html_())
@@ -1,12 +1,12 @@
1
1
  from typing import Union, TYPE_CHECKING
2
2
 
3
3
  if TYPE_CHECKING:
4
- from edsl.questions.QuestionBase import QuestionBase
5
-
6
- from edsl.surveys.Rule import Rule
7
- from .base import RulePriority, EndOfSurvey
8
- from edsl.exceptions.surveys import SurveyError, SurveyCreationError
4
+ from ...questions import QuestionBase
5
+ from ..survey import Survey
9
6
 
7
+ from ..exceptions import SurveyError, SurveyCreationError
8
+ from .rule import Rule
9
+ from ..base import RulePriority, EndOfSurvey
10
10
 
11
11
  class ValidatedString(str):
12
12
  def __new__(cls, content):
@@ -22,8 +22,8 @@ class RuleManager:
22
22
  self.survey = survey
23
23
 
24
24
  def _get_question_index(
25
- self, q: Union["QuestionBase", str, EndOfSurvey.__class__]
26
- ) -> Union[int, EndOfSurvey.__class__]:
25
+ self, q: Union["QuestionBase", str, 'EndOfSurvey']
26
+ ) -> Union[int, 'EndOfSurvey']:
27
27
  """Return the index of the question or EndOfSurvey object.
28
28
 
29
29
  :param q: The question or question name to get the index of.
@@ -41,7 +41,7 @@ class RuleManager:
41
41
  >>> s._get_question_index("poop")
42
42
  Traceback (most recent call last):
43
43
  ...
44
- edsl.exceptions.surveys.SurveyError: Question name poop not found in survey. The current question names are {'q0': 0, 'q1': 1, 'q2': 2}.
44
+ edsl.surveys.exceptions.SurveyError: Question name poop not found in survey. The current question names are {'q0': 0, 'q1': 1, 'q2': 2}.
45
45
  ...
46
46
  """
47
47
  if q == EndOfSurvey:
@@ -141,19 +141,19 @@ class RuleManager:
141
141
  Here, answering "yes" to q0 ends the survey:
142
142
 
143
143
  >>> from edsl import Survey
144
- >>> s = Survey.example().add_stop_rule("q0", "q0 == 'yes'")
145
- >>> s.next_question("q0", {"q0": "yes"})
144
+ >>> s = Survey.example().add_stop_rule("q0", "{{ q0.answer }} == 'yes'")
145
+ >>> s.next_question("q0", {"q0.answer": "yes"})
146
146
  EndOfSurvey
147
147
 
148
148
  By comparison, answering "no" to q0 does not end the survey:
149
149
 
150
- >>> s.next_question("q0", {"q0": "no"}).question_name
150
+ >>> s.next_question("q0", {"q0.answer": "no"}).question_name
151
151
  'q1'
152
152
 
153
- >>> s.add_stop_rule("q0", "q1 <> 'yes'")
153
+ >>> s.add_stop_rule("q0", "{{ q1.answer }} <> 'yes'")
154
154
  Traceback (most recent call last):
155
155
  ...
156
- edsl.exceptions.surveys.SurveyCreationError: The expression contains '<>', which is not allowed. You probably mean '!='.
156
+ edsl.surveys.exceptions.SurveyCreationError: The expression contains '<>', which is not allowed. You probably mean '!='.
157
157
  ...
158
158
  """
159
159
  expression = ValidatedString(expression)
@@ -170,3 +170,9 @@ class RuleManager:
170
170
  )
171
171
  self.survey.add_rule(question, expression, EndOfSurvey)
172
172
  return self.survey
173
+
174
+
175
+ if __name__ == "__main__":
176
+ import doctest
177
+
178
+ doctest.testmod(optionflags=doctest.ELLIPSIS)