edsl 0.1.49__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 +120 -29
  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.49.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.49.dist-info/RECORD +0 -347
  238. {edsl-0.1.49.dist-info → edsl-0.1.50.dist-info}/LICENSE +0 -0
  239. {edsl-0.1.49.dist-info → edsl-0.1.50.dist-info}/WHEEL +0 -0
edsl/base/base_class.py CHANGED
@@ -18,11 +18,12 @@ import json
18
18
  from typing import Any, Optional, Union
19
19
  from uuid import UUID
20
20
  import difflib
21
- import json
22
- from typing import Any, Dict, Tuple
21
+ from typing import Dict, Tuple
23
22
  from collections import UserList
24
23
  import inspect
25
24
 
25
+ from .. import logger
26
+
26
27
  class BaseException(Exception):
27
28
  """Base exception class for all EDSL exceptions.
28
29
 
@@ -35,12 +36,13 @@ class BaseException(Exception):
35
36
  """
36
37
  relevant_doc = "https://docs.expectedparrot.com/"
37
38
 
38
- def __init__(self, message, *, show_docs=True):
39
+ def __init__(self, message, *, show_docs=True, log_level="error"):
39
40
  """Initialize a new BaseException with formatted error message.
40
41
 
41
42
  Args:
42
43
  message: The primary error message
43
44
  show_docs: If True, append documentation links to the error message
45
+ log_level: The logging level to use ("debug", "info", "warning", "error", "critical")
44
46
  """
45
47
  # Format main error message
46
48
  formatted_message = [message.strip()]
@@ -59,6 +61,19 @@ class BaseException(Exception):
59
61
  # Join with double newlines for clear separation
60
62
  final_message = "\n\n".join(formatted_message)
61
63
  super().__init__(final_message)
64
+
65
+ # Log the exception
66
+ if log_level == "debug":
67
+ logger.debug(f"{self.__class__.__name__}: {message}")
68
+ elif log_level == "info":
69
+ logger.info(f"{self.__class__.__name__}: {message}")
70
+ elif log_level == "warning":
71
+ logger.warning(f"{self.__class__.__name__}: {message}")
72
+ elif log_level == "error":
73
+ logger.error(f"{self.__class__.__name__}: {message}")
74
+ elif log_level == "critical":
75
+ logger.critical(f"{self.__class__.__name__}: {message}")
76
+ # Default to error if an invalid log level is provided
62
77
 
63
78
 
64
79
  class DisplayJSON:
@@ -176,7 +191,7 @@ class PersistenceMixin:
176
191
  A new instance of the class populated with the deserialized data
177
192
 
178
193
  Raises:
179
- ValueError: If neither yaml_str nor filename is provided
194
+ BaseValueError: If neither yaml_str nor filename is provided
180
195
  """
181
196
  if yaml_str is None and filename is not None:
182
197
  with open(filename, "r") as f:
@@ -188,7 +203,8 @@ class PersistenceMixin:
188
203
  d = yaml.load(yaml_str, Loader=yaml.FullLoader)
189
204
  return cls.from_dict(d)
190
205
  else:
191
- raise ValueError("Either yaml_str or filename must be provided.")
206
+ from edsl.base.exceptions import BaseValueError
207
+ raise BaseValueError("Either yaml_str or filename must be provided.")
192
208
 
193
209
  def create_download_link(self):
194
210
  """Generate a downloadable link for this object.
@@ -365,23 +381,28 @@ class PersistenceMixin:
365
381
  >>> obj.save("my_object.json.gz") # Compressed
366
382
  >>> obj.save("my_object.json", compress=False) # Uncompressed
367
383
  """
384
+ logger.debug(f"Saving {self.__class__.__name__} to file: {filename}")
385
+
368
386
  if filename.endswith("json.gz"):
369
- import warnings
370
-
371
387
  filename = filename[:-8]
372
388
  if filename.endswith("json"):
373
389
  filename = filename[:-5]
374
390
 
375
- if compress:
376
- full_file_name = filename + ".json.gz"
377
- with gzip.open(full_file_name, "wb") as f:
378
- f.write(json.dumps(self.to_dict()).encode("utf-8"))
379
- else:
380
- full_file_name = filename + ".json"
381
- with open(filename + ".json", "w") as f:
382
- f.write(json.dumps(self.to_dict()))
383
-
384
- print("Saved to", full_file_name)
391
+ try:
392
+ if compress:
393
+ full_file_name = filename + ".json.gz"
394
+ with gzip.open(full_file_name, "wb") as f:
395
+ f.write(json.dumps(self.to_dict()).encode("utf-8"))
396
+ else:
397
+ full_file_name = filename + ".json"
398
+ with open(filename + ".json", "w") as f:
399
+ f.write(json.dumps(self.to_dict()))
400
+
401
+ logger.info(f"Successfully saved {self.__class__.__name__} to {full_file_name}")
402
+ print("Saved to", full_file_name)
403
+ except Exception as e:
404
+ logger.error(f"Failed to save {self.__class__.__name__} to {filename}: {str(e)}")
405
+ raise
385
406
 
386
407
  @staticmethod
387
408
  def open_compressed_file(filename):
@@ -429,19 +450,30 @@ class PersistenceMixin:
429
450
  Raises:
430
451
  Various exceptions may be raised if the file doesn't exist or contains invalid data
431
452
  """
432
- if filename.endswith("json.gz"):
433
- d = cls.open_compressed_file(filename)
434
- elif filename.endswith("json"):
435
- d = cls.open_regular_file(filename)
436
- else:
437
- try:
438
- d = cls.open_compressed_file(filename + ".json.gz")
439
- except:
440
- d = cls.open_regular_file(filename + ".json")
441
- # finally:
442
- # raise ValueError("File must be a json or json.gz file")
443
-
444
- return cls.from_dict(d)
453
+ logger.debug(f"Loading {cls.__name__} from file: {filename}")
454
+
455
+ try:
456
+ if filename.endswith("json.gz"):
457
+ d = cls.open_compressed_file(filename)
458
+ logger.debug(f"Loaded compressed file {filename}")
459
+ elif filename.endswith("json"):
460
+ d = cls.open_regular_file(filename)
461
+ logger.debug(f"Loaded regular file {filename}")
462
+ else:
463
+ try:
464
+ logger.debug(f"Attempting to load as compressed file: {filename}.json.gz")
465
+ d = cls.open_compressed_file(filename + ".json.gz")
466
+ except Exception as e:
467
+ logger.debug(f"Failed to load as compressed file, trying regular: {e}")
468
+ d = cls.open_regular_file(filename + ".json")
469
+ # finally:
470
+ # raise ValueError("File must be a json or json.gz file")
471
+
472
+ logger.info(f"Successfully loaded {cls.__name__} from {filename}")
473
+ return cls.from_dict(d)
474
+ except Exception as e:
475
+ logger.error(f"Failed to load {cls.__name__} from {filename}: {str(e)}")
476
+ raise
445
477
 
446
478
 
447
479
  class RegisterSubclassesMeta(ABCMeta):
@@ -739,7 +771,8 @@ class Base(
739
771
  Returns:
740
772
  An instance of the class with sample data
741
773
  """
742
- raise NotImplementedError("This method is not implemented yet.")
774
+ from edsl.base.exceptions import BaseNotImplementedError
775
+ raise BaseNotImplementedError("This method is not implemented yet.")
743
776
 
744
777
  def json(self):
745
778
  """Get a formatted JSON representation of this object.
@@ -755,7 +788,6 @@ class Base(
755
788
  Returns:
756
789
  DisplayYAML: A displayable YAML representation
757
790
  """
758
- import yaml
759
791
  return DisplayYAML(self.to_dict(add_edsl_version=False))
760
792
 
761
793
 
@@ -770,7 +802,8 @@ class Base(
770
802
  Returns:
771
803
  dict: A dictionary representation of the object
772
804
  """
773
- raise NotImplementedError("This method is not implemented yet.")
805
+ from edsl.base.exceptions import BaseNotImplementedError
806
+ raise BaseNotImplementedError("This method is not implemented yet.")
774
807
 
775
808
  def to_json(self):
776
809
  """Serialize this object to a JSON string.
@@ -806,7 +839,8 @@ class Base(
806
839
  Returns:
807
840
  An instance of the class populated with data from the dictionary
808
841
  """
809
- raise NotImplementedError("This method is not implemented yet.")
842
+ from edsl.base.exceptions import BaseNotImplementedError
843
+ raise BaseNotImplementedError("This method is not implemented yet.")
810
844
 
811
845
  @abstractmethod
812
846
  def code():
@@ -818,7 +852,8 @@ class Base(
818
852
  Returns:
819
853
  str: Python code that, when executed, creates an equivalent object
820
854
  """
821
- raise NotImplementedError("This method is not implemented yet.")
855
+ from edsl.base.exceptions import BaseNotImplementedError
856
+ raise BaseNotImplementedError("This method is not implemented yet.")
822
857
 
823
858
  def show_methods(self, show_docstrings=True):
824
859
  """Display all public methods available on this object.
@@ -1240,7 +1275,7 @@ class BaseDiff:
1240
1275
  result.append(f" Old value: {v1}")
1241
1276
  result.append(f" New value: {v2}")
1242
1277
  if diff:
1243
- result.append(f" Diff:")
1278
+ result.append(" Diff:")
1244
1279
  try:
1245
1280
  for line in diff:
1246
1281
  result.append(f" {line}")
@@ -0,0 +1,204 @@
1
+ import sys
2
+ from IPython.core.interactiveshell import InteractiveShell
3
+ from IPython.display import HTML, display
4
+ from pathlib import Path
5
+ import traceback
6
+
7
+ # Example logger import
8
+ from .. import logger
9
+
10
+
11
+ class BaseException(Exception):
12
+ """Base exception class for all EDSL exceptions.
13
+
14
+ This class extends the standard Python Exception class to provide more helpful error messages
15
+ by including links to relevant documentation and example notebooks when available.
16
+
17
+ Attributes:
18
+ relevant_doc: URL to documentation explaining this type of exception
19
+ relevant_notebook: Optional URL to a notebook with usage examples
20
+ doc_page: Optional string with the document page name (without extension)
21
+ doc_anchor: Optional string with the anchor within the document page
22
+ """
23
+
24
+ relevant_doc = "https://docs.expectedparrot.com/"
25
+ relevant_notebook = None # or set a default if you like
26
+ suppress_traceback = True
27
+ doc_page = None
28
+ doc_anchor = None
29
+
30
+ @classmethod
31
+ def get_doc_url(cls):
32
+ """Construct the documentation URL from the doc_page and doc_anchor attributes.
33
+
34
+ Returns:
35
+ str: The full documentation URL
36
+ """
37
+ base_url = "https://docs.expectedparrot.com/en/latest/"
38
+
39
+ if cls.doc_page:
40
+ url = f"{base_url}{cls.doc_page}.html"
41
+ if cls.doc_anchor:
42
+ url = f"{url}#{cls.doc_anchor}"
43
+ return url
44
+
45
+ return base_url
46
+
47
+ def __init__(
48
+ self,
49
+ message: str,
50
+ *,
51
+ show_docs: bool = True,
52
+ log_level: str = "error",
53
+ silent: bool = False,
54
+ ):
55
+ """
56
+ Initialize a new BaseException with a formatted error message.
57
+
58
+ Args:
59
+ message (str): The primary error message.
60
+ show_docs (bool): If True, append documentation links to the error message.
61
+ log_level (str): The logging level to use
62
+ ("debug", "info", "warning", "error", "critical").
63
+ silent (bool): If True, suppress all output when the exception is caught.
64
+ """
65
+ self.silent = silent
66
+
67
+ # Format main error message
68
+ formatted_message = [message.strip()]
69
+
70
+ # Add class docstring if available
71
+ if self.__class__.__doc__:
72
+ doc = self.__class__.__doc__.strip()
73
+ formatted_message.append(f"\n{doc}")
74
+
75
+ # Add documentation links if requested
76
+ if show_docs:
77
+ # Use the class method to get the documentation URL if doc_page is set
78
+ if hasattr(self.__class__, "doc_page") and self.__class__.doc_page:
79
+ formatted_message.append(
80
+ f"\nFor more information, see: {self.__class__.get_doc_url()}"
81
+ )
82
+ # Fall back to relevant_doc if it's explicitly set
83
+ elif hasattr(self, "relevant_doc"):
84
+ formatted_message.append(
85
+ f"\nFor more information, see: {self.relevant_doc}"
86
+ )
87
+ if self.relevant_notebook:
88
+ formatted_message.append(
89
+ f"\nFor a usage example, see: {self.relevant_notebook}"
90
+ )
91
+
92
+ # Join with double newlines for clear separation
93
+ final_message = "\n\n".join(formatted_message)
94
+ super().__init__(final_message)
95
+
96
+ # Log the exception unless silent is True
97
+ if not self.silent:
98
+ self._log_message(log_level, message)
99
+
100
+ @staticmethod
101
+ def _log_message(log_level: str, message: str):
102
+ """Helper to log a message at the specified log level."""
103
+ # Adjust as needed for your logger setup
104
+ if log_level == "debug":
105
+ logger.debug(message)
106
+ elif log_level == "info":
107
+ logger.info(message)
108
+ elif log_level == "warning":
109
+ logger.warning(message)
110
+ elif log_level == "error":
111
+ logger.error(message)
112
+ elif log_level == "critical":
113
+ logger.critical(message)
114
+
115
+ @classmethod
116
+ def install_exception_hook(cls):
117
+ """
118
+ Install custom exception handling for EDSL exceptions.
119
+
120
+ In an IPython/Jupyter environment, this uses `set_custom_exc` to handle
121
+ BaseException (and its subclasses). In a standard Python environment,
122
+ it falls back to overriding `sys.excepthook`.
123
+ """
124
+ if cls._in_ipython():
125
+ cls._install_ipython_hook()
126
+ else:
127
+ cls._install_sys_excepthook()
128
+
129
+ @classmethod
130
+ def _install_ipython_hook(cls):
131
+ """Use IPython's recommended approach for a custom exception handler."""
132
+
133
+ shell = InteractiveShell.instance()
134
+
135
+ # Wrap in a function so we can pass it to set_custom_exc.
136
+ def _ipython_custom_exc(shell, etype, evalue, tb, tb_offset=None):
137
+ if issubclass(etype, BaseException) and cls.suppress_traceback:
138
+ # Show custom message only if not silent
139
+ if not getattr(evalue, "silent", False):
140
+ # Try HTML display first; fall back to stderr
141
+ # try:
142
+ # display(
143
+ # HTML(
144
+ # f"<div style='color: red'>❌ EDSL ERROR: "
145
+ # f"{etype.__name__}: {evalue}</div>"
146
+ # )
147
+ # )
148
+ # except:
149
+ print(f"❌ EDSL ERROR: {etype.__name__}: {evalue}", file=sys.stderr)
150
+ # Suppress IPython’s normal traceback
151
+ return
152
+ # Otherwise, fall back to the usual traceback
153
+ return shell.showtraceback((etype, evalue, tb), tb_offset=tb_offset)
154
+
155
+ shell.set_custom_exc((BaseException,), _ipython_custom_exc)
156
+
157
+ @classmethod
158
+ def _install_sys_excepthook(cls):
159
+ """
160
+ Override the default sys.excepthook in a standard Python environment.
161
+ This is typically NOT recommended for IPython/Jupyter.
162
+ """
163
+ if getattr(sys, "custom_excepthook_installed", False):
164
+ return # Already installed
165
+
166
+ original_excepthook = sys.excepthook
167
+
168
+ def _custom_excepthook(exc_type, exc_value, exc_traceback):
169
+ if issubclass(exc_type, BaseException) and cls.suppress_traceback:
170
+ # Show custom message only if not silent
171
+ if not getattr(exc_value, "silent", False):
172
+ # try:
173
+ # display(
174
+ # HTML(
175
+ # f"<div style='color: red'>❌ EDSL ERROR: "
176
+ # f"{exc_type.__name__}: {exc_value}</div>"
177
+ # )
178
+ # )
179
+ # except:
180
+ print(
181
+ f"❌ EDSL ERROR: {exc_type.__name__}: {exc_value}",
182
+ exc_traceback,
183
+ file=sys.stderr,
184
+ )
185
+ # Suppress traceback
186
+ traceback.print_exception(
187
+ exc_type, exc_value, exc_traceback, file=sys.stderr
188
+ )
189
+
190
+ return
191
+ # Otherwise, use the default handler
192
+ return original_excepthook(exc_type, exc_value, exc_traceback)
193
+
194
+ sys.excepthook = _custom_excepthook
195
+ sys.custom_excepthook_installed = True
196
+
197
+ @staticmethod
198
+ def _in_ipython() -> bool:
199
+ """Return True if running inside IPython/Jupyter, False otherwise."""
200
+ try:
201
+ get_ipython() # noqa
202
+ return True
203
+ except NameError:
204
+ return False
@@ -1,5 +1,5 @@
1
1
  from collections import UserDict
2
- from typing import NamedTuple, Dict, List, Optional, Any
2
+ from typing import NamedTuple, Dict, Optional, Any
3
3
  from dataclasses import dataclass, fields
4
4
 
5
5
 
@@ -0,0 +1,94 @@
1
+ """
2
+ Base-specific exception classes.
3
+
4
+ This module defines custom exceptions for the base module, extending the BaseException
5
+ class to provide consistent error handling throughout the EDSL framework.
6
+ """
7
+
8
+ from .base_exception import BaseException
9
+
10
+ class BaseValueError(BaseException):
11
+ """
12
+ Exception raised for invalid values in base module operations.
13
+
14
+ This exception is raised when an operation in the base module encounters an
15
+ inappropriate value that prevents it from being completed successfully. It
16
+ replaces standard ValueError with domain-specific error handling.
17
+
18
+ Examples:
19
+ - Missing required parameters
20
+ - Parameter values outside acceptable ranges
21
+ - Incompatible parameter combinations
22
+ """
23
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/base.html"
24
+
25
+ def __init__(self, message="Invalid value provided", **kwargs):
26
+ super().__init__(message, **kwargs)
27
+
28
+ class BaseNotImplementedError(BaseException):
29
+ """
30
+ Exception raised when a method that should be implemented is not.
31
+
32
+ This exception is raised when calling an abstract or unimplemented method
33
+ in the base module. It should be used for methods that are expected to be
34
+ overridden by subclasses but have not been implemented yet.
35
+
36
+ Examples:
37
+ - Abstract methods called directly
38
+ - Interface methods not yet implemented
39
+ - Placeholder methods requiring implementation
40
+ """
41
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/base.html"
42
+
43
+ def __init__(self, message="This method is not implemented yet", **kwargs):
44
+ super().__init__(message, **kwargs)
45
+
46
+ class BaseKeyError(BaseException):
47
+ """
48
+ Exception raised for missing keys in base module operations.
49
+
50
+ This exception is raised when a required key is missing from a dictionary
51
+ or similar data structure in the base module.
52
+
53
+ Examples:
54
+ - Accessing a non-existent key in a configuration dictionary
55
+ - Missing required fields in serialized data
56
+ """
57
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/base.html"
58
+
59
+ def __init__(self, message="Required key not found", **kwargs):
60
+ super().__init__(message, **kwargs)
61
+
62
+ class BaseFileError(BaseException):
63
+ """
64
+ Exception raised for file-related errors in base module operations.
65
+
66
+ This exception is raised when file operations in the base module encounter
67
+ issues such as missing files, permission problems, or format errors.
68
+
69
+ Examples:
70
+ - File not found
71
+ - File format incompatible with the operation
72
+ - Permission denied when accessing a file
73
+ """
74
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/base.html"
75
+
76
+ def __init__(self, message="Error in file operation", **kwargs):
77
+ super().__init__(message, **kwargs)
78
+
79
+ class BaseTypeError(BaseException):
80
+ """
81
+ Exception raised for type-related errors in base module operations.
82
+
83
+ This exception is raised when an operation in the base module encounters
84
+ an inappropriate type that prevents it from being completed successfully.
85
+
86
+ Examples:
87
+ - Function arguments of incorrect type
88
+ - Type conversion failures
89
+ - Incompatible types in operations
90
+ """
91
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/base.html"
92
+
93
+ def __init__(self, message="Invalid type provided", **kwargs):
94
+ super().__init__(message, **kwargs)
edsl/buckets/__init__.py CHANGED
@@ -21,5 +21,19 @@ multiple processes or machines need to share rate limits.
21
21
  from .bucket_collection import BucketCollection
22
22
  from .model_buckets import ModelBuckets
23
23
  from .token_bucket import TokenBucket
24
+ from .exceptions import (
25
+ BucketError,
26
+ TokenLimitError,
27
+ TokenBucketClientError,
28
+ BucketConfigurationError,
29
+ )
24
30
 
25
- __all__ = ["BucketCollection", "ModelBuckets", "TokenBucket"]
31
+ __all__ = [
32
+ "BucketCollection",
33
+ "ModelBuckets",
34
+ "TokenBucket",
35
+ "BucketError",
36
+ "TokenLimitError",
37
+ "TokenBucketClientError",
38
+ "BucketConfigurationError",
39
+ ]
@@ -7,10 +7,9 @@ API rate limits are respected while allowing models from the same service to
7
7
  share the same rate limit buckets.
8
8
  """
9
9
 
10
- from typing import Optional, TYPE_CHECKING, Dict, List, Any, Tuple
10
+ from typing import TYPE_CHECKING, Dict, List, Tuple
11
11
  from collections import UserDict
12
12
  from threading import RLock
13
- import matplotlib.pyplot as plt
14
13
  from matplotlib.figure import Figure
15
14
 
16
15
  from .token_bucket import TokenBucket
@@ -74,7 +73,6 @@ class BucketCollection(UserDict):
74
73
  self._lock = RLock()
75
74
 
76
75
  # Check for remote token bucket server URL in environment
77
- from edsl.config import CONFIG
78
76
  import os
79
77
 
80
78
  url = os.environ.get("EDSL_REMOTE_TOKEN_BUCKET_URL", None)
@@ -137,7 +135,8 @@ class BucketCollection(UserDict):
137
135
  >>> # The following would raise an exception:
138
136
  >>> # bucket_collection.get_tokens(m, 'tokens', 10)
139
137
  """
140
- raise Exception("This method is deprecated and should not be used")
138
+ from edsl.buckets.exceptions import BucketError
139
+ raise BucketError("This method is deprecated and should not be used")
141
140
  # The following code is kept for reference only
142
141
  # relevant_bucket = getattr(self[model], bucket_type)
143
142
  # return relevant_bucket.get_tokens(num_tokens)
@@ -0,0 +1,75 @@
1
+ """
2
+ Exceptions module for bucket-related operations.
3
+
4
+ This module defines custom exception classes for all bucket-related error conditions
5
+ in the EDSL framework, ensuring consistent error handling for token bucket operations,
6
+ rate limiting, and related functionality.
7
+ """
8
+
9
+ from ..base import BaseException
10
+
11
+
12
+ class BucketError(BaseException):
13
+ """
14
+ Base exception class for all bucket-related errors.
15
+
16
+ This is the parent class for exceptions related to token bucket operations
17
+ in the EDSL framework, including token management, rate limiting, and related
18
+ functionality.
19
+
20
+ Examples:
21
+ ```python
22
+ # Usually not raised directly, but through subclasses:
23
+ bucket = TokenBucket(name="test", capacity=100, refill_rate=10)
24
+ bucket.get_tokens(200) # Would raise TokenLimitError
25
+ ```
26
+ """
27
+ relevant_doc = "https://docs.expectedparrot.com/"
28
+
29
+
30
+ class TokenLimitError(BucketError):
31
+ """
32
+ Exception raised when a token request exceeds available limits.
33
+
34
+ This exception occurs when attempting to request more tokens than
35
+ are currently available in the bucket or when exceeding rate limits.
36
+
37
+ Examples:
38
+ ```python
39
+ bucket = TokenBucket(name="test", capacity=100, refill_rate=10)
40
+ bucket.get_tokens(200) # Raises TokenLimitError
41
+ ```
42
+ """
43
+ relevant_doc = "https://docs.expectedparrot.com/"
44
+
45
+
46
+ class TokenBucketClientError(BucketError):
47
+ """
48
+ Exception raised when there's an error communicating with a remote token bucket.
49
+
50
+ This exception occurs when the client cannot connect to the token bucket service,
51
+ receives an unexpected response, or encounters other client-related issues.
52
+
53
+ Examples:
54
+ ```python
55
+ client = TokenBucketClient(url="invalid_url")
56
+ client.get_tokens(10) # May raise TokenBucketClientError
57
+ ```
58
+ """
59
+ relevant_doc = "https://docs.expectedparrot.com/"
60
+
61
+
62
+ class BucketConfigurationError(BucketError):
63
+ """
64
+ Exception raised when there's an issue with bucket configuration.
65
+
66
+ This exception occurs when bucket parameters are invalid, such as
67
+ negative capacity, invalid refill rates, or other configuration problems.
68
+
69
+ Examples:
70
+ ```python
71
+ # Attempting to create a bucket with invalid parameters:
72
+ TokenBucket(name="test", capacity=-100, refill_rate=10) # Would raise BucketConfigurationError
73
+ ```
74
+ """
75
+ relevant_doc = "https://docs.expectedparrot.com/"
@@ -6,8 +6,7 @@ and tokens-per-minute rate limits for language model API services. Each ModelBuc
6
6
  instance contains two TokenBucket instances - one for requests and one for tokens.
7
7
  """
8
8
 
9
- from typing import TYPE_CHECKING, Tuple, Optional, Any
10
- import matplotlib.pyplot as plt
9
+ from typing import TYPE_CHECKING, Tuple
11
10
  from matplotlib.figure import Figure
12
11
 
13
12
  if TYPE_CHECKING: