edsl 0.1.48__py3-none-any.whl → 0.1.50__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 (239) hide show
  1. edsl/__init__.py +124 -53
  2. edsl/__version__.py +1 -1
  3. edsl/agents/agent.py +21 -21
  4. edsl/agents/agent_list.py +2 -5
  5. edsl/agents/exceptions.py +119 -5
  6. edsl/base/__init__.py +10 -35
  7. edsl/base/base_class.py +71 -36
  8. edsl/base/base_exception.py +204 -0
  9. edsl/base/data_transfer_models.py +1 -1
  10. edsl/base/exceptions.py +94 -0
  11. edsl/buckets/__init__.py +15 -1
  12. edsl/buckets/bucket_collection.py +3 -4
  13. edsl/buckets/exceptions.py +75 -0
  14. edsl/buckets/model_buckets.py +1 -2
  15. edsl/buckets/token_bucket.py +11 -6
  16. edsl/buckets/token_bucket_api.py +1 -2
  17. edsl/buckets/token_bucket_client.py +9 -7
  18. edsl/caching/cache.py +7 -2
  19. edsl/caching/cache_entry.py +10 -9
  20. edsl/caching/exceptions.py +113 -7
  21. edsl/caching/remote_cache_sync.py +1 -2
  22. edsl/caching/sql_dict.py +17 -12
  23. edsl/cli.py +43 -0
  24. edsl/config/config_class.py +30 -6
  25. edsl/conversation/Conversation.py +3 -2
  26. edsl/conversation/exceptions.py +58 -0
  27. edsl/conversation/mug_negotiation.py +0 -2
  28. edsl/coop/__init__.py +20 -1
  29. edsl/coop/coop.py +129 -38
  30. edsl/coop/exceptions.py +188 -9
  31. edsl/coop/price_fetcher.py +3 -6
  32. edsl/coop/utils.py +4 -6
  33. edsl/dataset/__init__.py +5 -4
  34. edsl/dataset/dataset.py +53 -43
  35. edsl/dataset/dataset_operations_mixin.py +86 -72
  36. edsl/dataset/dataset_tree.py +9 -5
  37. edsl/dataset/display/table_display.py +0 -2
  38. edsl/dataset/display/table_renderers.py +0 -1
  39. edsl/dataset/exceptions.py +125 -0
  40. edsl/dataset/file_exports.py +18 -11
  41. edsl/dataset/r/ggplot.py +13 -6
  42. edsl/display/__init__.py +27 -0
  43. edsl/display/core.py +147 -0
  44. edsl/display/plugin.py +189 -0
  45. edsl/display/utils.py +52 -0
  46. edsl/inference_services/__init__.py +9 -1
  47. edsl/inference_services/available_model_cache_handler.py +1 -1
  48. edsl/inference_services/available_model_fetcher.py +4 -5
  49. edsl/inference_services/data_structures.py +9 -6
  50. edsl/inference_services/exceptions.py +132 -1
  51. edsl/inference_services/inference_service_abc.py +2 -2
  52. edsl/inference_services/inference_services_collection.py +2 -6
  53. edsl/inference_services/registry.py +4 -3
  54. edsl/inference_services/service_availability.py +2 -1
  55. edsl/inference_services/services/anthropic_service.py +4 -1
  56. edsl/inference_services/services/aws_bedrock.py +13 -12
  57. edsl/inference_services/services/azure_ai.py +12 -10
  58. edsl/inference_services/services/deep_infra_service.py +1 -4
  59. edsl/inference_services/services/deep_seek_service.py +1 -5
  60. edsl/inference_services/services/google_service.py +6 -2
  61. edsl/inference_services/services/groq_service.py +1 -1
  62. edsl/inference_services/services/mistral_ai_service.py +4 -2
  63. edsl/inference_services/services/ollama_service.py +1 -1
  64. edsl/inference_services/services/open_ai_service.py +7 -5
  65. edsl/inference_services/services/perplexity_service.py +6 -2
  66. edsl/inference_services/services/test_service.py +8 -7
  67. edsl/inference_services/services/together_ai_service.py +2 -3
  68. edsl/inference_services/services/xai_service.py +1 -1
  69. edsl/instructions/__init__.py +1 -1
  70. edsl/instructions/change_instruction.py +3 -2
  71. edsl/instructions/exceptions.py +61 -0
  72. edsl/instructions/instruction.py +5 -2
  73. edsl/instructions/instruction_collection.py +2 -1
  74. edsl/instructions/instruction_handler.py +4 -9
  75. edsl/interviews/ReportErrors.py +0 -3
  76. edsl/interviews/__init__.py +9 -2
  77. edsl/interviews/answering_function.py +11 -13
  78. edsl/interviews/exception_tracking.py +14 -7
  79. edsl/interviews/exceptions.py +79 -0
  80. edsl/interviews/interview.py +32 -29
  81. edsl/interviews/interview_status_dictionary.py +4 -2
  82. edsl/interviews/interview_status_log.py +2 -1
  83. edsl/interviews/interview_task_manager.py +3 -3
  84. edsl/interviews/request_token_estimator.py +3 -1
  85. edsl/interviews/statistics.py +2 -3
  86. edsl/invigilators/__init__.py +7 -1
  87. edsl/invigilators/exceptions.py +79 -0
  88. edsl/invigilators/invigilator_base.py +0 -1
  89. edsl/invigilators/invigilators.py +8 -12
  90. edsl/invigilators/prompt_constructor.py +1 -5
  91. edsl/invigilators/prompt_helpers.py +8 -4
  92. edsl/invigilators/question_instructions_prompt_builder.py +1 -1
  93. edsl/invigilators/question_option_processor.py +9 -5
  94. edsl/invigilators/question_template_replacements_builder.py +3 -2
  95. edsl/jobs/__init__.py +3 -3
  96. edsl/jobs/async_interview_runner.py +24 -22
  97. edsl/jobs/check_survey_scenario_compatibility.py +7 -6
  98. edsl/jobs/data_structures.py +7 -4
  99. edsl/jobs/exceptions.py +177 -8
  100. edsl/jobs/fetch_invigilator.py +1 -1
  101. edsl/jobs/jobs.py +72 -67
  102. edsl/jobs/jobs_checks.py +2 -3
  103. edsl/jobs/jobs_component_constructor.py +2 -2
  104. edsl/jobs/jobs_pricing_estimation.py +3 -2
  105. edsl/jobs/jobs_remote_inference_logger.py +5 -4
  106. edsl/jobs/jobs_runner_asyncio.py +1 -2
  107. edsl/jobs/jobs_runner_status.py +8 -9
  108. edsl/jobs/remote_inference.py +26 -23
  109. edsl/jobs/results_exceptions_handler.py +8 -5
  110. edsl/key_management/__init__.py +3 -1
  111. edsl/key_management/exceptions.py +62 -0
  112. edsl/key_management/key_lookup.py +1 -1
  113. edsl/key_management/key_lookup_builder.py +37 -14
  114. edsl/key_management/key_lookup_collection.py +2 -0
  115. edsl/language_models/__init__.py +1 -1
  116. edsl/language_models/exceptions.py +302 -14
  117. edsl/language_models/language_model.py +4 -7
  118. edsl/language_models/model.py +4 -4
  119. edsl/language_models/model_list.py +1 -1
  120. edsl/language_models/price_manager.py +1 -1
  121. edsl/language_models/raw_response_handler.py +14 -9
  122. edsl/language_models/registry.py +17 -21
  123. edsl/language_models/repair.py +0 -6
  124. edsl/language_models/unused/fake_openai_service.py +0 -1
  125. edsl/load_plugins.py +69 -0
  126. edsl/logger.py +146 -0
  127. edsl/notebooks/notebook.py +1 -1
  128. edsl/notebooks/notebook_to_latex.py +0 -1
  129. edsl/plugins/__init__.py +63 -0
  130. edsl/plugins/built_in/export_example.py +50 -0
  131. edsl/plugins/built_in/pig_latin.py +67 -0
  132. edsl/plugins/cli.py +372 -0
  133. edsl/plugins/cli_typer.py +283 -0
  134. edsl/plugins/exceptions.py +31 -0
  135. edsl/plugins/hookspec.py +51 -0
  136. edsl/plugins/plugin_host.py +128 -0
  137. edsl/plugins/plugin_manager.py +633 -0
  138. edsl/plugins/plugins_registry.py +168 -0
  139. edsl/prompts/__init__.py +2 -0
  140. edsl/prompts/exceptions.py +107 -5
  141. edsl/prompts/prompt.py +14 -6
  142. edsl/questions/HTMLQuestion.py +5 -11
  143. edsl/questions/Quick.py +0 -1
  144. edsl/questions/__init__.py +2 -0
  145. edsl/questions/answer_validator_mixin.py +318 -318
  146. edsl/questions/compose_questions.py +2 -2
  147. edsl/questions/descriptors.py +10 -49
  148. edsl/questions/exceptions.py +278 -22
  149. edsl/questions/loop_processor.py +7 -5
  150. edsl/questions/prompt_templates/question_list.jinja +3 -0
  151. edsl/questions/question_base.py +14 -16
  152. edsl/questions/question_base_gen_mixin.py +2 -2
  153. edsl/questions/question_base_prompts_mixin.py +9 -3
  154. edsl/questions/question_budget.py +9 -5
  155. edsl/questions/question_check_box.py +3 -5
  156. edsl/questions/question_dict.py +171 -194
  157. edsl/questions/question_extract.py +1 -1
  158. edsl/questions/question_free_text.py +4 -6
  159. edsl/questions/question_functional.py +4 -3
  160. edsl/questions/question_list.py +36 -9
  161. edsl/questions/question_matrix.py +95 -61
  162. edsl/questions/question_multiple_choice.py +6 -4
  163. edsl/questions/question_numerical.py +2 -4
  164. edsl/questions/question_registry.py +4 -2
  165. edsl/questions/register_questions_meta.py +0 -1
  166. edsl/questions/response_validator_abc.py +7 -13
  167. edsl/questions/templates/dict/answering_instructions.jinja +1 -0
  168. edsl/questions/templates/rank/question_presentation.jinja +1 -1
  169. edsl/results/__init__.py +1 -1
  170. edsl/results/exceptions.py +141 -7
  171. edsl/results/report.py +0 -1
  172. edsl/results/result.py +4 -5
  173. edsl/results/results.py +10 -51
  174. edsl/results/results_selector.py +8 -4
  175. edsl/scenarios/PdfExtractor.py +2 -2
  176. edsl/scenarios/construct_download_link.py +69 -35
  177. edsl/scenarios/directory_scanner.py +33 -14
  178. edsl/scenarios/document_chunker.py +1 -1
  179. edsl/scenarios/exceptions.py +238 -14
  180. edsl/scenarios/file_methods.py +1 -1
  181. edsl/scenarios/file_store.py +7 -3
  182. edsl/scenarios/handlers/__init__.py +17 -0
  183. edsl/scenarios/handlers/docx_file_store.py +0 -5
  184. edsl/scenarios/handlers/pdf_file_store.py +0 -1
  185. edsl/scenarios/handlers/pptx_file_store.py +0 -5
  186. edsl/scenarios/handlers/py_file_store.py +0 -1
  187. edsl/scenarios/handlers/sql_file_store.py +1 -4
  188. edsl/scenarios/handlers/sqlite_file_store.py +0 -1
  189. edsl/scenarios/handlers/txt_file_store.py +1 -1
  190. edsl/scenarios/scenario.py +0 -1
  191. edsl/scenarios/scenario_list.py +152 -18
  192. edsl/scenarios/scenario_list_pdf_tools.py +1 -0
  193. edsl/scenarios/scenario_selector.py +0 -1
  194. edsl/surveys/__init__.py +3 -4
  195. edsl/surveys/dag/__init__.py +4 -2
  196. edsl/surveys/descriptors.py +1 -1
  197. edsl/surveys/edit_survey.py +1 -0
  198. edsl/surveys/exceptions.py +165 -9
  199. edsl/surveys/memory/__init__.py +5 -3
  200. edsl/surveys/memory/memory_management.py +1 -0
  201. edsl/surveys/memory/memory_plan.py +6 -15
  202. edsl/surveys/rules/__init__.py +5 -3
  203. edsl/surveys/rules/rule.py +1 -2
  204. edsl/surveys/rules/rule_collection.py +1 -1
  205. edsl/surveys/survey.py +12 -24
  206. edsl/surveys/survey_export.py +6 -3
  207. edsl/surveys/survey_flow_visualization.py +10 -1
  208. edsl/tasks/__init__.py +2 -0
  209. edsl/tasks/question_task_creator.py +3 -3
  210. edsl/tasks/task_creators.py +1 -3
  211. edsl/tasks/task_history.py +5 -7
  212. edsl/tasks/task_status_log.py +1 -2
  213. edsl/tokens/__init__.py +3 -1
  214. edsl/tokens/token_usage.py +1 -1
  215. edsl/utilities/__init__.py +21 -1
  216. edsl/utilities/decorators.py +1 -2
  217. edsl/utilities/markdown_to_docx.py +2 -2
  218. edsl/utilities/markdown_to_pdf.py +1 -1
  219. edsl/utilities/repair_functions.py +0 -1
  220. edsl/utilities/restricted_python.py +0 -1
  221. edsl/utilities/template_loader.py +2 -3
  222. edsl/utilities/utilities.py +8 -29
  223. {edsl-0.1.48.dist-info → edsl-0.1.50.dist-info}/METADATA +32 -2
  224. edsl-0.1.50.dist-info/RECORD +363 -0
  225. edsl-0.1.50.dist-info/entry_points.txt +3 -0
  226. edsl/dataset/smart_objects.py +0 -96
  227. edsl/exceptions/BaseException.py +0 -21
  228. edsl/exceptions/__init__.py +0 -54
  229. edsl/exceptions/configuration.py +0 -16
  230. edsl/exceptions/general.py +0 -34
  231. edsl/study/ObjectEntry.py +0 -173
  232. edsl/study/ProofOfWork.py +0 -113
  233. edsl/study/SnapShot.py +0 -80
  234. edsl/study/Study.py +0 -520
  235. edsl/study/__init__.py +0 -6
  236. edsl/utilities/interface.py +0 -135
  237. edsl-0.1.48.dist-info/RECORD +0 -347
  238. {edsl-0.1.48.dist-info → edsl-0.1.50.dist-info}/LICENSE +0 -0
  239. {edsl-0.1.48.dist-info → edsl-0.1.50.dist-info}/WHEEL +0 -0
@@ -30,6 +30,8 @@ from typing import (
30
30
  import warnings
31
31
  import csv
32
32
  import random
33
+ import os
34
+ import glob
33
35
  from io import StringIO
34
36
  import inspect
35
37
  from collections import UserList, defaultdict
@@ -57,8 +59,9 @@ from ..dataset import ScenarioListOperationsMixin
57
59
 
58
60
  from .exceptions import ScenarioError
59
61
  from .scenario import Scenario
60
- from .directory_scanner import DirectoryScanner
61
62
  from .scenario_list_pdf_tools import PdfTools
63
+ from .directory_scanner import DirectoryScanner
64
+ from .file_store import FileStore
62
65
 
63
66
 
64
67
  if TYPE_CHECKING:
@@ -887,6 +890,119 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
887
890
  sl = self.duplicate()
888
891
  return ScenarioList([scenario.keep(fields) for scenario in sl])
889
892
 
893
+ @classmethod
894
+ def from_directory(
895
+ cls,
896
+ path: Optional[str] = None,
897
+ recursive: bool = False,
898
+ ) -> "ScenarioList":
899
+ """Create a ScenarioList of FileStore objects from files in a directory.
900
+
901
+ This method scans a directory and creates a FileStore object for each file found,
902
+ optionally filtering files based on a wildcard pattern. If no path is provided,
903
+ the current working directory is used.
904
+
905
+ Args:
906
+ path: The directory path to scan, optionally including a wildcard pattern.
907
+ If None, uses the current working directory.
908
+ Examples:
909
+ - "/path/to/directory" - scans all files in the directory
910
+ - "/path/to/directory/*.py" - scans only Python files in the directory
911
+ - "*.txt" - scans only text files in the current working directory
912
+ recursive: Whether to scan subdirectories recursively. Defaults to False.
913
+
914
+ Returns:
915
+ A ScenarioList containing FileStore objects for all matching files.
916
+
917
+ Raises:
918
+ FileNotFoundError: If the specified directory does not exist.
919
+
920
+ Examples:
921
+ # Get all files in the current directory
922
+ sl = ScenarioList.from_directory()
923
+
924
+ # Get all Python files in a specific directory
925
+ sl = ScenarioList.from_directory('*.py')
926
+
927
+ # Get all image files in the current directory
928
+ sl = ScenarioList.from_directory('*.png')
929
+
930
+ # Get all files recursively including subdirectories
931
+ sl = ScenarioList.from_directory(recursive=True)
932
+ """
933
+ # Handle default case - use current directory
934
+ if path is None:
935
+ directory_path = os.getcwd()
936
+ pattern = None
937
+ else:
938
+ # Special handling for "**" pattern which indicates recursive scanning
939
+ has_recursive_pattern = '**' in path if path else False
940
+
941
+ # Check if path contains any wildcard
942
+ if path and ('*' in path):
943
+ # Handle "**/*.ext" pattern - find the directory part before the **
944
+ if has_recursive_pattern:
945
+ # Extract the base directory by finding the part before **
946
+ parts = path.split('**')
947
+ if parts and parts[0]:
948
+ # Remove trailing slash if any
949
+ directory_path = parts[0].rstrip('/')
950
+ if not directory_path:
951
+ directory_path = os.getcwd()
952
+ # Get the pattern after **
953
+ pattern = parts[1] if len(parts) > 1 else None
954
+ if pattern and pattern.startswith('/'):
955
+ pattern = pattern[1:] # Remove leading slash
956
+ else:
957
+ directory_path = os.getcwd()
958
+ pattern = None
959
+ # Handle case where path is just a pattern (e.g., "*.py")
960
+ elif os.path.dirname(path) == '':
961
+ directory_path = os.getcwd()
962
+ pattern = os.path.basename(path)
963
+ else:
964
+ # Split into directory and pattern
965
+ directory_path = os.path.dirname(path)
966
+ if not directory_path:
967
+ directory_path = os.getcwd()
968
+ pattern = os.path.basename(path)
969
+ else:
970
+ # Path is a directory with no pattern
971
+ directory_path = path
972
+ pattern = None
973
+
974
+ # Ensure directory exists
975
+ if not os.path.isdir(directory_path):
976
+ raise FileNotFoundError(f"Directory not found: {directory_path}")
977
+
978
+ # Create a DirectoryScanner for the directory
979
+ scanner = DirectoryScanner(directory_path)
980
+
981
+ # Configure wildcard pattern filtering
982
+ suffix_allow_list = None
983
+ example_suffix = None
984
+
985
+ if pattern:
986
+ if pattern.startswith('*.'):
987
+ # Simple extension filter (e.g., "*.py")
988
+ suffix_allow_list = [pattern[2:]]
989
+ elif '*' in pattern:
990
+ # Other wildcard patterns
991
+ example_suffix = pattern
992
+ else:
993
+ # Handle simple non-wildcard pattern (exact match)
994
+ example_suffix = pattern
995
+
996
+ # Use scanner to find files and create FileStore objects
997
+ file_stores = scanner.scan(
998
+ factory=lambda path: FileStore(path),
999
+ recursive=recursive,
1000
+ suffix_allow_list=suffix_allow_list,
1001
+ example_suffix=example_suffix
1002
+ )
1003
+
1004
+ return cls(file_stores)
1005
+
890
1006
  @classmethod
891
1007
  def from_list(
892
1008
  cls, name: str, values: list, func: Optional[Callable] = None
@@ -903,7 +1019,9 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
903
1019
  ScenarioList([Scenario({'name': 'Alice'}), Scenario({'name': 'Bob'})])
904
1020
  """
905
1021
  if not func:
906
- func = lambda x: x
1022
+ def identity(x):
1023
+ return x
1024
+ func = identity
907
1025
  return cls([Scenario({name: func(value)}) for value in values])
908
1026
 
909
1027
  def table(
@@ -914,7 +1032,6 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
914
1032
  ) -> str:
915
1033
  """Return the ScenarioList as a table."""
916
1034
 
917
- from tabulate import tabulate_formats
918
1035
 
919
1036
  if tablefmt is not None and tablefmt not in tabulate_formats:
920
1037
  raise ValueError(
@@ -951,13 +1068,10 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
951
1068
 
952
1069
  Example:
953
1070
 
954
- >>> s = ScenarioList([Scenario({'a': 1, 'b': 2}), Scenario({'a': 3, 'b': 4})])
955
- >>> s.reorder_keys(['b', 'a'])
956
- ScenarioList([Scenario({'b': 2, 'a': 1}), Scenario({'b': 4, 'a': 3})])
957
- >>> s.reorder_keys(['a', 'b', 'c'])
958
- Traceback (most recent call last):
959
- ...
960
- AssertionError
1071
+ # Example:
1072
+ # s = ScenarioList([Scenario({'a': 1, 'b': 2}), Scenario({'a': 3, 'b': 4})])
1073
+ # s.reorder_keys(['b', 'a']) # Returns a new ScenarioList with reordered keys
1074
+ # Attempting s.reorder_keys(['a', 'b', 'c']) would fail as 'c' is not a valid key
961
1075
  """
962
1076
  assert set(new_order) == set(self.parameters)
963
1077
 
@@ -1020,7 +1134,7 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
1020
1134
  return ScenarioList(new_scenarios)
1021
1135
 
1022
1136
  @classmethod
1023
- def from_list_of_tuples(self, *names: str, values: List[Tuple]) -> ScenarioList:
1137
+ def from_list_of_tuples(self, *names: str, values: List[tuple]) -> ScenarioList:
1024
1138
  sl = ScenarioList.from_list(names[0], [value[0] for value in values])
1025
1139
  for index, name in enumerate(names[1:]):
1026
1140
  sl = sl.add_list(name, [value[index + 1] for value in values])
@@ -1210,7 +1324,6 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
1210
1324
  """
1211
1325
  import tempfile
1212
1326
  import requests
1213
- from docx import Document
1214
1327
 
1215
1328
  if "/edit" in url:
1216
1329
  doc_id = url.split("/d/")[1].split("/edit")[0]
@@ -1468,7 +1581,6 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
1468
1581
  ScenarioList: An instance of the ScenarioList class.
1469
1582
 
1470
1583
  """
1471
- import pandas as pd
1472
1584
  import tempfile
1473
1585
  import requests
1474
1586
 
@@ -1542,14 +1654,37 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
1542
1654
  response.raise_for_status()
1543
1655
  file_obj = StringIO(response.text)
1544
1656
  else:
1545
- file_obj = open(source, "r")
1657
+ # Try different encodings if the default fails
1658
+ encodings_to_try = ["utf-8", "latin-1", "cp1252", "ISO-8859-1"]
1659
+ last_exception = None
1660
+ file_obj = None
1661
+
1662
+ for encoding in encodings_to_try:
1663
+ try:
1664
+ file_obj = open(source, "r", encoding=encoding)
1665
+ # Test reading a bit to verify encoding
1666
+ file_obj.readline()
1667
+ file_obj.seek(0) # Reset file position
1668
+ break
1669
+ except UnicodeDecodeError as e:
1670
+ last_exception = e
1671
+ if file_obj:
1672
+ file_obj.close()
1673
+ file_obj = None
1674
+
1675
+ if file_obj is None:
1676
+ raise ValueError(f"Could not decode file {source} with any of the attempted encodings. Original error: {last_exception}")
1546
1677
 
1547
1678
  reader = csv.reader(file_obj, delimiter=delimiter)
1548
- header = next(reader)
1549
- observations = [Scenario(dict(zip(header, row))) for row in reader]
1679
+ try:
1680
+ header = next(reader)
1681
+ observations = [Scenario(dict(zip(header, row))) for row in reader]
1682
+ except StopIteration:
1683
+ raise ValueError(f"File {source} appears to be empty or has an invalid format")
1550
1684
 
1551
1685
  finally:
1552
- file_obj.close()
1686
+ if file_obj:
1687
+ file_obj.close()
1553
1688
 
1554
1689
  return cls(observations)
1555
1690
 
@@ -1614,7 +1749,6 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
1614
1749
  """
1615
1750
  from ..surveys import Survey
1616
1751
  from ..questions import QuestionBase
1617
- from ..jobs import Jobs
1618
1752
 
1619
1753
  if isinstance(survey, QuestionBase):
1620
1754
  return Survey([survey]).by(self)
@@ -175,6 +175,7 @@ class PdfTools:
175
175
  image_path = os.path.join(output_folder, f"page_{i+1}.{image_format}")
176
176
  image.save(image_path, image_format.upper())
177
177
 
178
+ from ..file_store import FileStore
178
179
  scenario = Scenario({
179
180
  "filepath":image_path,
180
181
  "page":i,
@@ -3,7 +3,6 @@ from typing import TYPE_CHECKING
3
3
 
4
4
  if TYPE_CHECKING:
5
5
  from .scenario_list import ScenarioList
6
- from .scenario import Scenario
7
6
 
8
7
  class ScenarioSelector:
9
8
  """
edsl/surveys/__init__.py CHANGED
@@ -1,8 +1,7 @@
1
1
  from .survey import Survey
2
- from .survey_flow_visualization import SurveyFlowVisualization
3
-
4
- from .rules import Rule, RuleCollection
5
- from .base import EndOfSurvey, RulePriority
2
+ from .survey_flow_visualization import SurveyFlowVisualization # noqa: F401
3
+ from .rules import Rule, RuleCollection # noqa: F401
4
+ from .base import EndOfSurvey, RulePriority # noqa: F401
6
5
 
7
6
  __all__ = ["Survey"]
8
7
  ## , "SurveyFlowVisualization", "Rule", "RuleCollection", "EndOfSurvey", "RulePriority"]
@@ -1,2 +1,4 @@
1
- from .dag import DAG
2
- from .construct_dag import ConstructDAG
1
+ from .dag import DAG # noqa: F401
2
+ from .construct_dag import ConstructDAG # noqa: F401
3
+
4
+ __all__ = []
@@ -4,7 +4,7 @@ from abc import ABC, abstractmethod
4
4
  from typing import Any, TYPE_CHECKING
5
5
 
6
6
  if TYPE_CHECKING:
7
- from edsl.questions.QuestionBase import QuestionBase
7
+ pass # Not using any imported types in this file
8
8
 
9
9
 
10
10
  class BaseDescriptor(ABC):
@@ -5,6 +5,7 @@ from typing import Union, Optional, TYPE_CHECKING
5
5
  if TYPE_CHECKING:
6
6
  from ..questions import QuestionBase
7
7
  from .survey import Survey
8
+ from ..instructions import Instruction, ChangeInstruction
8
9
 
9
10
  from .exceptions import SurveyError, SurveyCreationError
10
11
  from .rules.rule import Rule
@@ -1,36 +1,192 @@
1
1
  from ..base import BaseException
2
2
 
3
3
  class SurveyError(BaseException):
4
- relevant_doc = "https://docs.expectedparrot.com/en/latest/surveys.html"
4
+ """
5
+ Base exception class for all survey-related errors.
6
+
7
+ This exception is the parent class for all exceptions related to Survey operations,
8
+ including creation, validation, and navigation. It provides a common type
9
+ for catching any survey-specific error.
10
+
11
+ This exception is raised directly when:
12
+ - Question names don't meet validation requirements
13
+ - Survey operations encounter general errors not covered by more specific exceptions
14
+ """
15
+ doc_page = "surveys"
5
16
 
6
17
 
7
18
  class SurveyCreationError(SurveyError):
8
- pass
19
+ """
20
+ Exception raised when there's an error creating or modifying a survey.
21
+
22
+ This exception occurs when:
23
+ - Adding skip rules to EndOfSurvey (which isn't allowed)
24
+ - Combining surveys with non-default rules
25
+ - Adding questions with duplicate names
26
+ - Creating invalid question groups
27
+ - Validating group names and boundaries
28
+
29
+ To fix this error:
30
+ 1. Ensure all question names in a survey are unique
31
+ 2. Don't add skip rules to EndOfSurvey
32
+ 3. Check that question groups are properly defined
33
+ 4. When combining surveys, ensure they're compatible
34
+
35
+ Examples:
36
+ ```python
37
+ survey.add(question) # Raises SurveyCreationError if question's name already exists
38
+ survey.add_rules_to_question(EndOfSurvey) # Raises SurveyCreationError
39
+ ```
40
+ """
41
+ doc_anchor = "creating-surveys"
9
42
 
10
43
 
11
44
  class SurveyHasNoRulesError(SurveyError):
12
- pass
45
+ """
46
+ Exception raised when rules are required but not found for a question.
47
+
48
+ This exception occurs when:
49
+ - The survey's next_question method is called but no rules exist for the current question
50
+ - Navigation can't proceed because there's no defined path forward
51
+
52
+ To fix this error:
53
+ 1. Add appropriate rules to all questions in the survey
54
+ 2. Ensure the survey has a complete navigation path from start to finish
55
+ 3. Use default rules where appropriate (add_default_rules method)
56
+
57
+ Examples:
58
+ ```python
59
+ survey.next_question(0) # Raises SurveyHasNoRulesError if question 0 has no rules
60
+ ```
61
+ """
62
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/surveys.html#rules"
13
63
 
14
64
 
15
65
  class SurveyRuleSendsYouBackwardsError(SurveyError):
16
- pass
66
+ """
67
+ Exception raised when a rule would navigate backward in the survey flow.
68
+
69
+ This exception occurs during rule initialization to prevent rules that
70
+ would navigate backward in the survey flow, which is not allowed.
71
+
72
+ Backward navigation creates potential loops and is generally considered
73
+ poor survey design. EDSL enforces forward-only navigation.
74
+
75
+ To fix this error:
76
+ 1. Redesign your survey to avoid backward navigation
77
+ 2. Use forward-only rules with proper branching logic
78
+ 3. Consider using memory to carry forward information if needed
79
+
80
+ Examples:
81
+ ```python
82
+ survey.add_rule(question_index=2, rule=Rule(lambda x: True, 1)) # Raises SurveyRuleSendsYouBackwardsError
83
+ ```
84
+ """
85
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/surveys.html#rules"
17
86
 
18
87
 
19
88
  class SurveyRuleSkipLogicSyntaxError(SurveyError):
20
- pass
89
+ """
90
+ Exception raised when a rule's expression has invalid syntax.
91
+
92
+ This exception occurs when:
93
+ - The expression in a rule is not valid Python syntax
94
+ - The expression can't be parsed or compiled
95
+
96
+ To fix this error:
97
+ 1. Check the syntax of your rule expression
98
+ 2. Ensure all variables in the expression are properly referenced
99
+ 3. Test the expression in isolation to verify it's valid Python
100
+
101
+ Examples:
102
+ ```python
103
+ Rule(lambda x: x[question] ==, 1) # Raises SurveyRuleSkipLogicSyntaxError (invalid syntax)
104
+ ```
105
+ """
106
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/surveys.html#rules"
21
107
 
22
108
 
23
109
  class SurveyRuleReferenceInRuleToUnknownQuestionError(SurveyError):
24
- pass
110
+ """
111
+ Exception raised when a rule references an unknown question.
112
+
113
+ This exception is designed to catch cases where a rule's condition
114
+ references a question that doesn't exist in the survey.
115
+
116
+ To fix this error:
117
+ 1. Ensure all questions referenced in rule conditions exist in the survey
118
+ 2. Check for typos in question names or indices
119
+
120
+ Note: This exception is defined but not actively used in the codebase.
121
+ It raises Exception("not used") to indicate this state.
122
+ """
123
+ def __init__(self, message="Rule references an unknown question", **kwargs):
124
+ super().__init__(message, **kwargs)
25
125
 
26
126
 
27
127
  class SurveyRuleRefersToFutureStateError(SurveyError):
28
- pass
128
+ """
129
+ Exception raised when a rule references questions that come later in the survey.
130
+
131
+ This exception occurs when:
132
+ - A rule condition refers to questions that haven't been presented yet
133
+ - Rule evaluation would require information not yet collected
134
+
135
+ To fix this error:
136
+ 1. Redesign your rules to only reference current or previous questions
137
+ 2. Ensure rule conditions only depend on information already collected
138
+ 3. Restructure your survey if you need different branching logic
139
+
140
+ Examples:
141
+ ```python
142
+ # If question 3 hasn't been asked yet:
143
+ Rule(lambda x: x['q3_answer'] == 'Yes', next_question=4) # Raises SurveyRuleRefersToFutureStateError
144
+ ```
145
+ """
146
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/surveys.html#rules"
29
147
 
30
148
 
31
149
  class SurveyRuleCollectionHasNoRulesAtNodeError(SurveyError):
32
- pass
150
+ """
151
+ Exception raised when no rules are found for a specific question during navigation.
152
+
153
+ This exception occurs when:
154
+ - The RuleCollection's next_question method can't find applicable rules
155
+ - A survey is trying to determine the next question but has no rule for the current state
156
+
157
+ To fix this error:
158
+ 1. Add rules for all questions in your survey
159
+ 2. Ensure rules are properly added to the RuleCollection
160
+ 3. Add default rules where appropriate
161
+
162
+ Examples:
163
+ ```python
164
+ # If rule_collection has no rules for question 2:
165
+ rule_collection.next_question(2, {}) # Raises SurveyRuleCollectionHasNoRulesAtNodeError
166
+ ```
167
+ """
168
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/surveys.html#rule-collections"
33
169
 
34
170
 
35
171
  class SurveyRuleCannotEvaluateError(SurveyError):
36
- pass
172
+ """
173
+ Exception raised when a rule expression cannot be evaluated.
174
+
175
+ This exception occurs when:
176
+ - The rule's expression fails to evaluate with the provided data
177
+ - Required variables are missing in the evaluation context
178
+ - The expression contains errors that only appear at runtime
179
+
180
+ To fix this error:
181
+ 1. Check that your rule expression is valid
182
+ 2. Ensure all referenced variables are available in the context
183
+ 3. Add error handling in complex expressions
184
+ 4. Test rules with sample data before using in production
185
+
186
+ Examples:
187
+ ```python
188
+ # If 'q1_answer' is not in the data dictionary:
189
+ Rule(lambda x: x['q1_answer'] == 'Yes', 2).evaluate({}) # Raises SurveyRuleCannotEvaluateError
190
+ ```
191
+ """
192
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/surveys.html#rules"
@@ -1,3 +1,5 @@
1
- from .memory_plan import MemoryPlan
2
- from .memory_management import MemoryManagement
3
- from .memory import Memory
1
+ from .memory_plan import MemoryPlan # noqa: F401
2
+ from .memory_management import MemoryManagement # noqa: F401
3
+ from .memory import Memory # noqa: F401
4
+
5
+ __all__ = []
@@ -3,6 +3,7 @@ from typing import Callable, Union, List, TYPE_CHECKING
3
3
 
4
4
  if TYPE_CHECKING:
5
5
  from edsl.questions.QuestionBase import QuestionBase
6
+ from ...surveys.survey import Survey
6
7
 
7
8
 
8
9
  class MemoryManagement:
@@ -8,6 +8,7 @@ if TYPE_CHECKING:
8
8
  from ...surveys import Survey
9
9
  from ...prompts import Prompt
10
10
  from ..dag import DAG
11
+ from ...questions.question_base import QuestionBase
11
12
 
12
13
  class MemoryPlan(UserDict):
13
14
  """A survey has a memory plan that specifies what the agent should remember when answering a question.
@@ -103,23 +104,13 @@ class MemoryPlan(UserDict):
103
104
  :param focal_question: The current question being answered.
104
105
  :param prior_question: The question that was answered before the focal question that should be remembered.
105
106
 
106
- >>> mp = MemoryPlan.example()
107
- >>> mp.add_single_memory("q0", "q1")
108
- Traceback (most recent call last):
109
- ...
110
- ValueError: q1 must come before q0.
107
+ # Prior question must come before focal question
108
+ # Example: adding "q0", "q1" would raise ValueError as q1 comes after q0
111
109
 
112
- >>> mp = MemoryPlan.example()
113
- >>> mp.add_single_memory("q0", "crap")
114
- Traceback (most recent call last):
115
- ...
116
- ValueError: crap is not in the survey. Current names are ['q0', 'q1', 'q2']
110
+ # Question names must exist in the survey
111
+ # Example: using a non-existent question name "crap" would raise ValueError
117
112
 
118
- >>> mp = MemoryPlan.example()
119
- >>> mp.add_single_memory("crap", "q0")
120
- Traceback (most recent call last):
121
- ...
122
- ValueError: crap is not in the survey. Current names are ['q0', 'q1', 'q2']
113
+ # Similarly, focal question must also exist in the survey
123
114
  """
124
115
  self._check_valid_question_name(focal_question)
125
116
  self._check_valid_question_name(prior_question)
@@ -1,3 +1,5 @@
1
- from .rule import Rule
2
- from .rule_manager import RuleManager
3
- from .rule_collection import RuleCollection
1
+ from .rule import Rule # noqa: F401
2
+ from .rule_manager import RuleManager # noqa: F401
3
+ from .rule_collection import RuleCollection # noqa: F401
4
+
5
+ __all__ = []
@@ -19,7 +19,7 @@ with a low (-1) priority.
19
19
 
20
20
  import ast
21
21
  import random
22
- from typing import Any, Union, List
22
+ from typing import Any, Union
23
23
  from collections import defaultdict
24
24
 
25
25
 
@@ -27,7 +27,6 @@ from collections import defaultdict
27
27
  from simpleeval import EvalWithCompoundTypes
28
28
 
29
29
  from ..exceptions import SurveyError
30
-
31
30
  from ..exceptions import (
32
31
  SurveyRuleCannotEvaluateError,
33
32
  SurveyRuleRefersToFutureStateError,
@@ -1,6 +1,6 @@
1
1
  """A collection of rules for a survey."""
2
2
 
3
- from typing import List, Union, Any, Optional
3
+ from typing import List, Any, Optional
4
4
  from collections import defaultdict, UserList, namedtuple
5
5
 
6
6
  from ..exceptions import (