edsl 0.1.49__py3-none-any.whl → 0.1.51__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 (257) 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 +107 -0
  14. edsl/buckets/model_buckets.py +1 -2
  15. edsl/buckets/token_bucket.py +11 -6
  16. edsl/buckets/token_bucket_api.py +27 -12
  17. edsl/buckets/token_bucket_client.py +9 -7
  18. edsl/caching/cache.py +12 -4
  19. edsl/caching/cache_entry.py +10 -9
  20. edsl/caching/exceptions.py +113 -7
  21. edsl/caching/remote_cache_sync.py +6 -7
  22. edsl/caching/sql_dict.py +20 -14
  23. edsl/cli.py +43 -0
  24. edsl/config/__init__.py +1 -1
  25. edsl/config/config_class.py +32 -6
  26. edsl/conversation/Conversation.py +8 -4
  27. edsl/conversation/car_buying.py +1 -3
  28. edsl/conversation/exceptions.py +58 -0
  29. edsl/conversation/mug_negotiation.py +2 -8
  30. edsl/coop/__init__.py +28 -6
  31. edsl/coop/coop.py +120 -29
  32. edsl/coop/coop_functions.py +1 -1
  33. edsl/coop/ep_key_handling.py +1 -1
  34. edsl/coop/exceptions.py +188 -9
  35. edsl/coop/price_fetcher.py +5 -8
  36. edsl/coop/utils.py +4 -6
  37. edsl/dataset/__init__.py +5 -4
  38. edsl/dataset/dataset.py +177 -86
  39. edsl/dataset/dataset_operations_mixin.py +98 -76
  40. edsl/dataset/dataset_tree.py +11 -7
  41. edsl/dataset/display/table_display.py +0 -2
  42. edsl/dataset/display/table_renderers.py +6 -4
  43. edsl/dataset/exceptions.py +125 -0
  44. edsl/dataset/file_exports.py +18 -11
  45. edsl/dataset/r/ggplot.py +13 -6
  46. edsl/display/__init__.py +27 -0
  47. edsl/display/core.py +147 -0
  48. edsl/display/plugin.py +189 -0
  49. edsl/display/utils.py +52 -0
  50. edsl/inference_services/__init__.py +9 -1
  51. edsl/inference_services/available_model_cache_handler.py +1 -1
  52. edsl/inference_services/available_model_fetcher.py +5 -6
  53. edsl/inference_services/data_structures.py +10 -7
  54. edsl/inference_services/exceptions.py +132 -1
  55. edsl/inference_services/inference_service_abc.py +2 -2
  56. edsl/inference_services/inference_services_collection.py +2 -6
  57. edsl/inference_services/registry.py +4 -3
  58. edsl/inference_services/service_availability.py +4 -3
  59. edsl/inference_services/services/anthropic_service.py +4 -1
  60. edsl/inference_services/services/aws_bedrock.py +13 -12
  61. edsl/inference_services/services/azure_ai.py +12 -10
  62. edsl/inference_services/services/deep_infra_service.py +1 -4
  63. edsl/inference_services/services/deep_seek_service.py +1 -5
  64. edsl/inference_services/services/google_service.py +7 -3
  65. edsl/inference_services/services/groq_service.py +1 -1
  66. edsl/inference_services/services/mistral_ai_service.py +4 -2
  67. edsl/inference_services/services/ollama_service.py +1 -1
  68. edsl/inference_services/services/open_ai_service.py +7 -5
  69. edsl/inference_services/services/perplexity_service.py +6 -2
  70. edsl/inference_services/services/test_service.py +8 -7
  71. edsl/inference_services/services/together_ai_service.py +2 -3
  72. edsl/inference_services/services/xai_service.py +1 -1
  73. edsl/instructions/__init__.py +1 -1
  74. edsl/instructions/change_instruction.py +7 -5
  75. edsl/instructions/exceptions.py +61 -0
  76. edsl/instructions/instruction.py +6 -2
  77. edsl/instructions/instruction_collection.py +6 -4
  78. edsl/instructions/instruction_handler.py +12 -15
  79. edsl/interviews/ReportErrors.py +0 -3
  80. edsl/interviews/__init__.py +9 -2
  81. edsl/interviews/answering_function.py +11 -13
  82. edsl/interviews/exception_tracking.py +15 -8
  83. edsl/interviews/exceptions.py +79 -0
  84. edsl/interviews/interview.py +33 -30
  85. edsl/interviews/interview_status_dictionary.py +4 -2
  86. edsl/interviews/interview_status_log.py +2 -1
  87. edsl/interviews/interview_task_manager.py +5 -5
  88. edsl/interviews/request_token_estimator.py +5 -2
  89. edsl/interviews/statistics.py +3 -4
  90. edsl/invigilators/__init__.py +7 -1
  91. edsl/invigilators/exceptions.py +79 -0
  92. edsl/invigilators/invigilator_base.py +0 -1
  93. edsl/invigilators/invigilators.py +9 -13
  94. edsl/invigilators/prompt_constructor.py +1 -5
  95. edsl/invigilators/prompt_helpers.py +8 -4
  96. edsl/invigilators/question_instructions_prompt_builder.py +1 -1
  97. edsl/invigilators/question_option_processor.py +9 -5
  98. edsl/invigilators/question_template_replacements_builder.py +3 -2
  99. edsl/jobs/__init__.py +42 -5
  100. edsl/jobs/async_interview_runner.py +25 -23
  101. edsl/jobs/check_survey_scenario_compatibility.py +11 -10
  102. edsl/jobs/data_structures.py +8 -5
  103. edsl/jobs/exceptions.py +177 -8
  104. edsl/jobs/fetch_invigilator.py +1 -1
  105. edsl/jobs/jobs.py +74 -69
  106. edsl/jobs/jobs_checks.py +6 -7
  107. edsl/jobs/jobs_component_constructor.py +4 -4
  108. edsl/jobs/jobs_pricing_estimation.py +4 -3
  109. edsl/jobs/jobs_remote_inference_logger.py +5 -4
  110. edsl/jobs/jobs_runner_asyncio.py +3 -4
  111. edsl/jobs/jobs_runner_status.py +8 -9
  112. edsl/jobs/remote_inference.py +27 -24
  113. edsl/jobs/results_exceptions_handler.py +10 -7
  114. edsl/key_management/__init__.py +3 -1
  115. edsl/key_management/exceptions.py +62 -0
  116. edsl/key_management/key_lookup.py +1 -1
  117. edsl/key_management/key_lookup_builder.py +37 -14
  118. edsl/key_management/key_lookup_collection.py +2 -0
  119. edsl/language_models/__init__.py +1 -1
  120. edsl/language_models/exceptions.py +302 -14
  121. edsl/language_models/language_model.py +9 -8
  122. edsl/language_models/model.py +4 -4
  123. edsl/language_models/model_list.py +1 -1
  124. edsl/language_models/price_manager.py +1 -1
  125. edsl/language_models/raw_response_handler.py +14 -9
  126. edsl/language_models/registry.py +17 -21
  127. edsl/language_models/repair.py +0 -6
  128. edsl/language_models/unused/fake_openai_service.py +0 -1
  129. edsl/load_plugins.py +69 -0
  130. edsl/logger.py +146 -0
  131. edsl/notebooks/__init__.py +24 -1
  132. edsl/notebooks/exceptions.py +82 -0
  133. edsl/notebooks/notebook.py +7 -3
  134. edsl/notebooks/notebook_to_latex.py +1 -2
  135. edsl/plugins/__init__.py +63 -0
  136. edsl/plugins/built_in/export_example.py +50 -0
  137. edsl/plugins/built_in/pig_latin.py +67 -0
  138. edsl/plugins/cli.py +372 -0
  139. edsl/plugins/cli_typer.py +283 -0
  140. edsl/plugins/exceptions.py +31 -0
  141. edsl/plugins/hookspec.py +51 -0
  142. edsl/plugins/plugin_host.py +128 -0
  143. edsl/plugins/plugin_manager.py +633 -0
  144. edsl/plugins/plugins_registry.py +168 -0
  145. edsl/prompts/__init__.py +24 -1
  146. edsl/prompts/exceptions.py +107 -5
  147. edsl/prompts/prompt.py +15 -7
  148. edsl/questions/HTMLQuestion.py +5 -11
  149. edsl/questions/Quick.py +0 -1
  150. edsl/questions/__init__.py +6 -4
  151. edsl/questions/answer_validator_mixin.py +318 -323
  152. edsl/questions/compose_questions.py +3 -3
  153. edsl/questions/descriptors.py +11 -50
  154. edsl/questions/exceptions.py +278 -22
  155. edsl/questions/loop_processor.py +7 -5
  156. edsl/questions/prompt_templates/question_list.jinja +3 -0
  157. edsl/questions/question_base.py +46 -19
  158. edsl/questions/question_base_gen_mixin.py +2 -2
  159. edsl/questions/question_base_prompts_mixin.py +13 -7
  160. edsl/questions/question_budget.py +503 -98
  161. edsl/questions/question_check_box.py +660 -160
  162. edsl/questions/question_dict.py +345 -194
  163. edsl/questions/question_extract.py +401 -61
  164. edsl/questions/question_free_text.py +80 -14
  165. edsl/questions/question_functional.py +119 -9
  166. edsl/questions/{derived/question_likert_five.py → question_likert_five.py} +2 -2
  167. edsl/questions/{derived/question_linear_scale.py → question_linear_scale.py} +3 -4
  168. edsl/questions/question_list.py +275 -28
  169. edsl/questions/question_matrix.py +643 -96
  170. edsl/questions/question_multiple_choice.py +219 -51
  171. edsl/questions/question_numerical.py +361 -32
  172. edsl/questions/question_rank.py +401 -124
  173. edsl/questions/question_registry.py +7 -5
  174. edsl/questions/{derived/question_top_k.py → question_top_k.py} +3 -3
  175. edsl/questions/{derived/question_yes_no.py → question_yes_no.py} +3 -4
  176. edsl/questions/register_questions_meta.py +2 -2
  177. edsl/questions/response_validator_abc.py +13 -15
  178. edsl/questions/response_validator_factory.py +10 -12
  179. edsl/questions/templates/dict/answering_instructions.jinja +1 -0
  180. edsl/questions/templates/rank/question_presentation.jinja +1 -1
  181. edsl/results/__init__.py +1 -1
  182. edsl/results/exceptions.py +141 -7
  183. edsl/results/report.py +1 -2
  184. edsl/results/result.py +11 -9
  185. edsl/results/results.py +480 -321
  186. edsl/results/results_selector.py +8 -4
  187. edsl/scenarios/PdfExtractor.py +2 -2
  188. edsl/scenarios/construct_download_link.py +69 -35
  189. edsl/scenarios/directory_scanner.py +33 -14
  190. edsl/scenarios/document_chunker.py +1 -1
  191. edsl/scenarios/exceptions.py +238 -14
  192. edsl/scenarios/file_methods.py +1 -1
  193. edsl/scenarios/file_store.py +7 -3
  194. edsl/scenarios/handlers/__init__.py +17 -0
  195. edsl/scenarios/handlers/docx_file_store.py +0 -5
  196. edsl/scenarios/handlers/pdf_file_store.py +0 -1
  197. edsl/scenarios/handlers/pptx_file_store.py +0 -5
  198. edsl/scenarios/handlers/py_file_store.py +0 -1
  199. edsl/scenarios/handlers/sql_file_store.py +1 -4
  200. edsl/scenarios/handlers/sqlite_file_store.py +0 -1
  201. edsl/scenarios/handlers/txt_file_store.py +1 -1
  202. edsl/scenarios/scenario.py +1 -3
  203. edsl/scenarios/scenario_list.py +179 -27
  204. edsl/scenarios/scenario_list_pdf_tools.py +1 -0
  205. edsl/scenarios/scenario_selector.py +0 -1
  206. edsl/surveys/__init__.py +3 -4
  207. edsl/surveys/dag/__init__.py +4 -2
  208. edsl/surveys/descriptors.py +1 -1
  209. edsl/surveys/edit_survey.py +1 -0
  210. edsl/surveys/exceptions.py +165 -9
  211. edsl/surveys/memory/__init__.py +5 -3
  212. edsl/surveys/memory/memory_management.py +1 -0
  213. edsl/surveys/memory/memory_plan.py +6 -15
  214. edsl/surveys/rules/__init__.py +5 -3
  215. edsl/surveys/rules/rule.py +1 -2
  216. edsl/surveys/rules/rule_collection.py +1 -1
  217. edsl/surveys/survey.py +12 -24
  218. edsl/surveys/survey_css.py +3 -3
  219. edsl/surveys/survey_export.py +6 -3
  220. edsl/surveys/survey_flow_visualization.py +10 -1
  221. edsl/surveys/survey_simulator.py +2 -1
  222. edsl/tasks/__init__.py +23 -1
  223. edsl/tasks/exceptions.py +72 -0
  224. edsl/tasks/question_task_creator.py +3 -3
  225. edsl/tasks/task_creators.py +1 -3
  226. edsl/tasks/task_history.py +8 -10
  227. edsl/tasks/task_status_log.py +1 -2
  228. edsl/tokens/__init__.py +29 -1
  229. edsl/tokens/exceptions.py +37 -0
  230. edsl/tokens/interview_token_usage.py +3 -2
  231. edsl/tokens/token_usage.py +4 -3
  232. edsl/utilities/__init__.py +21 -1
  233. edsl/utilities/decorators.py +1 -2
  234. edsl/utilities/markdown_to_docx.py +2 -2
  235. edsl/utilities/markdown_to_pdf.py +1 -1
  236. edsl/utilities/repair_functions.py +0 -1
  237. edsl/utilities/restricted_python.py +0 -1
  238. edsl/utilities/template_loader.py +2 -3
  239. edsl/utilities/utilities.py +8 -29
  240. {edsl-0.1.49.dist-info → edsl-0.1.51.dist-info}/METADATA +32 -2
  241. edsl-0.1.51.dist-info/RECORD +365 -0
  242. edsl-0.1.51.dist-info/entry_points.txt +3 -0
  243. edsl/dataset/smart_objects.py +0 -96
  244. edsl/exceptions/BaseException.py +0 -21
  245. edsl/exceptions/__init__.py +0 -54
  246. edsl/exceptions/configuration.py +0 -16
  247. edsl/exceptions/general.py +0 -34
  248. edsl/questions/derived/__init__.py +0 -0
  249. edsl/study/ObjectEntry.py +0 -173
  250. edsl/study/ProofOfWork.py +0 -113
  251. edsl/study/SnapShot.py +0 -80
  252. edsl/study/Study.py +0 -520
  253. edsl/study/__init__.py +0 -6
  254. edsl/utilities/interface.py +0 -135
  255. edsl-0.1.49.dist-info/RECORD +0 -347
  256. {edsl-0.1.49.dist-info → edsl-0.1.51.dist-info}/LICENSE +0 -0
  257. {edsl-0.1.49.dist-info → edsl-0.1.51.dist-info}/WHEEL +0 -0
@@ -1,11 +1,11 @@
1
1
  from typing import Union, Sequence, TYPE_CHECKING
2
+ from .exceptions import JobsValueError
2
3
 
3
4
  if TYPE_CHECKING:
4
5
  from ..agents import Agent
5
6
  from ..language_models import LanguageModel
6
7
  from ..scenarios import Scenario
7
8
  from .jobs import Jobs
8
- from ..invigilators import InvigilatorBase
9
9
 
10
10
  class JobsComponentConstructor:
11
11
  "Handles the creation of Agents, Scenarios, and LanguageModels in a job."
@@ -132,7 +132,7 @@ class JobsComponentConstructor:
132
132
  key = class_to_key[class_type]
133
133
  break
134
134
  else:
135
- raise ValueError(
135
+ raise JobsValueError(
136
136
  f"First argument must be an Agent, Scenario, or LanguageModel, not {object}"
137
137
  )
138
138
  current_objects = getattr(self.jobs, key, None)
@@ -140,8 +140,8 @@ class JobsComponentConstructor:
140
140
 
141
141
  @staticmethod
142
142
  def _get_empty_container_object(object):
143
- from edsl.agents import AgentList
144
- from edsl.scenarios import ScenarioList
143
+ from ..agents import AgentList
144
+ from ..scenarios import ScenarioList
145
145
 
146
146
  return {"Agent": AgentList([]), "Scenario": ScenarioList([])}.get(
147
147
  object.__class__.__name__, []
@@ -8,7 +8,8 @@ if TYPE_CHECKING:
8
8
  from ..agents import AgentList
9
9
  from ..scenarios import ScenarioList
10
10
  from ..surveys import Survey
11
- from .interviews.Interview import Interview
11
+ from ..interviews import Interview
12
+ from ..invigilators.invigilator_base import Invigilator
12
13
 
13
14
  from .fetch_invigilator import FetchInvigilator
14
15
  from ..caching import CacheEntry
@@ -136,13 +137,13 @@ class JobsPrompts:
136
137
  def price_lookup(self) -> dict:
137
138
  """Fetches the price lookup from Coop if it is not already cached."""
138
139
  if self._price_lookup is None:
139
- from edsl.coop.coop import Coop
140
+ from ..coop.coop import Coop
140
141
 
141
142
  c = Coop()
142
143
  self._price_lookup = c.fetch_prices()
143
144
  return self._price_lookup
144
145
 
145
- def _process_one_invigilator(self, invigilator: 'Invigilator', interview_index: int, iterations: int = 1) -> dict :
146
+ def _process_one_invigilator(self, invigilator: 'Invigilator', interview_index: int, iterations: int = 1) -> dict:
146
147
  """Process a single invigilator and return a dictionary with all needed data fields."""
147
148
  prompts = invigilator.get_prompts()
148
149
  user_prompt = prompts["user_prompt"]
@@ -2,16 +2,17 @@ import re
2
2
  import sys
3
3
  import uuid
4
4
  from abc import ABC, abstractmethod
5
- from typing import Optional, Union, Literal, TYPE_CHECKING, List, Dict
5
+ from typing import Literal, TYPE_CHECKING, List
6
6
  from datetime import datetime
7
7
  from dataclasses import dataclass
8
8
 
9
- from ..coop import CoopServerResponseError
9
+ from .exceptions import JobsValueError
10
+
10
11
 
11
12
  from .jobs_status_enums import JobsStatus
12
13
 
13
14
  if TYPE_CHECKING:
14
- from ..results import Results
15
+ pass
15
16
 
16
17
 
17
18
  @dataclass
@@ -63,7 +64,7 @@ class JobLogger(ABC):
63
64
  '1234'
64
65
  """
65
66
  if information_type not in self.jobs_info.__annotations__:
66
- raise ValueError(f"Information type {information_type} not supported")
67
+ raise JobsValueError(f"Information type {information_type} not supported")
67
68
  setattr(self.jobs_info, information_type, value)
68
69
 
69
70
  @abstractmethod
@@ -21,7 +21,7 @@ import time
21
21
  import asyncio
22
22
  import threading
23
23
  import warnings
24
- from typing import TYPE_CHECKING, List, Generator, Tuple, Optional, Any
24
+ from typing import TYPE_CHECKING, Optional
25
25
 
26
26
  if TYPE_CHECKING:
27
27
  from ..results import Results
@@ -36,7 +36,6 @@ from .data_structures import RunEnvironment, RunParameters, RunConfig
36
36
 
37
37
  if TYPE_CHECKING:
38
38
  from ..jobs import Jobs
39
- from ..interviews import Interview
40
39
 
41
40
 
42
41
  class JobsRunnerAsyncio:
@@ -187,7 +186,7 @@ class JobsRunnerAsyncio:
187
186
  self.start_time = time.monotonic()
188
187
  self.completed = False
189
188
 
190
- from edsl.coop import Coop
189
+ from ..coop import Coop
191
190
 
192
191
  coop = Coop()
193
192
  endpoint_url = coop.get_progress_bar_url()
@@ -274,7 +273,7 @@ class JobsRunnerAsyncio:
274
273
  # breakpoint()
275
274
  results.bucket_collection = self.environment.bucket_collection
276
275
 
277
- from edsl.jobs.results_exceptions_handler import ResultsExceptionsHandler
276
+ from .results_exceptions_handler import ResultsExceptionsHandler
278
277
 
279
278
  results_exceptions_handler = ResultsExceptionsHandler(results, parameters)
280
279
 
@@ -6,9 +6,12 @@ import requests
6
6
  from abc import ABC, abstractmethod
7
7
  from dataclasses import dataclass
8
8
  from collections import defaultdict
9
- from typing import Any, Dict, Optional
9
+ from typing import Any, Dict, Optional, TYPE_CHECKING
10
10
  from uuid import UUID
11
11
 
12
+ if TYPE_CHECKING:
13
+ from .jobs_runner_asyncio import JobsRunnerAsyncio
14
+
12
15
 
13
16
  @dataclass
14
17
  class ModelInfo:
@@ -171,16 +174,12 @@ class JobsRunnerStatusBase(ABC):
171
174
  status_dict["language_model_queues"] = model_queues
172
175
  return status_dict
173
176
 
174
- def add_completed_interview(self, result):
177
+ def add_completed_interview(self, interview):
175
178
  """Records a completed interview without storing the full interview data."""
176
179
  self.stats_tracker.add_completed_interview(
177
- model=result.model.model,
178
- num_exceptions=(
179
- len(result.exceptions) if hasattr(result, "exceptions") else 0
180
- ),
181
- num_unfixed=(
182
- result.exceptions.num_unfixed() if hasattr(result, "exceptions") else 0
183
- ),
180
+ model=interview.model.model,
181
+ num_exceptions=interview.exceptions.num_exceptions(),
182
+ num_unfixed=interview.exceptions.num_unfixed_exceptions(),
184
183
  )
185
184
 
186
185
  def _compute_statistic(self, stat_name: str):
@@ -1,6 +1,11 @@
1
1
  from typing import Optional, Union, Literal, TYPE_CHECKING, NewType, Callable, Any
2
-
3
2
  from dataclasses import dataclass
3
+ from ..coop import CoopServerResponseError
4
+ from ..coop.utils import VisibilityType
5
+ from ..coop.coop import RemoteInferenceResponse, RemoteInferenceCreationInfo
6
+ from .jobs_status_enums import JobsStatus
7
+ from .jobs_remote_inference_logger import JobLogger
8
+ from .exceptions import RemoteInferenceError
4
9
 
5
10
 
6
11
  Seconds = NewType("Seconds", float)
@@ -9,14 +14,7 @@ JobUUID = NewType("JobUUID", str)
9
14
  if TYPE_CHECKING:
10
15
  from ..results import Results
11
16
  from .jobs import Jobs
12
- from .jobs_remote_inference_logger import JobLogger
13
-
14
- from ..coop import CoopServerResponseError
15
- from ..coop.utils import VisibilityType
16
- from ..coop.coop import RemoteInferenceResponse, RemoteInferenceCreationInfo
17
17
 
18
- from .jobs_status_enums import JobsStatus
19
- from .jobs_remote_inference_logger import JobLogger
20
18
 
21
19
  class RemoteJobConstants:
22
20
  """Constants for remote job handling."""
@@ -45,7 +43,7 @@ class JobsRemoteInferenceHandler:
45
43
  self.verbose = verbose
46
44
  self.poll_interval = poll_interval
47
45
 
48
- from edsl.config import CONFIG
46
+ from ..config import CONFIG
49
47
 
50
48
  self.expected_parrot_url = CONFIG.get("EXPECTED_PARROT_URL")
51
49
  self.remote_inference_url = f"{self.expected_parrot_url}/home/remote-inference"
@@ -53,7 +51,6 @@ class JobsRemoteInferenceHandler:
53
51
  def _create_logger(self) -> JobLogger:
54
52
  from ..utilities import is_notebook
55
53
  from .jobs_remote_inference_logger import (
56
- JupyterJobLogger,
57
54
  StdOutJobLogger,
58
55
  )
59
56
  from .html_table_job_logger import HTMLTableJobLogger
@@ -75,7 +72,7 @@ class JobsRemoteInferenceHandler:
75
72
  return user_edsl_settings.get("remote_inference", False)
76
73
  except requests.ConnectionError:
77
74
  pass
78
- except CoopServerResponseError as e:
75
+ except CoopServerResponseError:
79
76
  pass
80
77
 
81
78
  return False
@@ -84,10 +81,9 @@ class JobsRemoteInferenceHandler:
84
81
  self,
85
82
  iterations: int = 1,
86
83
  remote_inference_description: Optional[str] = None,
87
- remote_inference_results_visibility: Optional['VisibilityType'] = "unlisted",
84
+ remote_inference_results_visibility: Optional["VisibilityType"] = "unlisted",
88
85
  fresh: Optional[bool] = False,
89
86
  ) -> RemoteJobInfo:
90
- from ..config import CONFIG
91
87
  from ..coop import Coop
92
88
 
93
89
  logger = self._create_logger()
@@ -137,7 +133,7 @@ class JobsRemoteInferenceHandler:
137
133
  @staticmethod
138
134
  def check_status(
139
135
  job_uuid: JobUUID,
140
- ) -> 'RemoteInferenceResponse':
136
+ ) -> "RemoteInferenceResponse":
141
137
  from ..coop import Coop
142
138
 
143
139
  coop = Coop()
@@ -182,6 +178,13 @@ class JobsRemoteInferenceHandler:
182
178
  ) -> None:
183
179
  "Handles a failed job by logging the error and updating the job status."
184
180
  latest_error_report_url = remote_job_data.get("latest_error_report_url")
181
+
182
+ reason = remote_job_data.get("reason")
183
+
184
+ if reason == "insufficient funds":
185
+ latest_error_report_url = "Error: Insufficient balance to start the job"
186
+ print("❌ Error: Insufficient balance to start the job")
187
+
185
188
  if latest_error_report_url:
186
189
  job_info.logger.add_info("error_report_url", latest_error_report_url)
187
190
 
@@ -235,13 +238,13 @@ class JobsRemoteInferenceHandler:
235
238
  """Makes one attempt to fetch and process a remote job's status and results."""
236
239
  remote_job_data = remote_job_data_fetcher(job_info.job_uuid)
237
240
  status = remote_job_data.get("status")
238
-
241
+ reason = remote_job_data.get("reason")
239
242
  if status == "cancelled":
240
243
  self._handle_cancelled_job(job_info)
241
- return None
244
+ return None, reason
242
245
 
243
- elif status == "failed" or status == "completed":
244
- if status == "failed":
246
+ elif status == "failed" or status == "completed" or status == "partial_failed":
247
+ if status == "failed" or status == "partial_failed":
245
248
  self._handle_failed_job(job_info, remote_job_data)
246
249
 
247
250
  results_uuid = remote_job_data.get("results_uuid")
@@ -252,13 +255,13 @@ class JobsRemoteInferenceHandler:
252
255
  remote_job_data=remote_job_data,
253
256
  object_fetcher=object_fetcher,
254
257
  )
255
- return results
258
+ return results, reason
256
259
  else:
257
- return None
260
+ return None, reason
258
261
 
259
262
  else:
260
263
  self._sleep_for_a_bit(job_info, status)
261
- return "continue"
264
+ return "continue", reason
262
265
 
263
266
  def poll_remote_inference_job(
264
267
  self,
@@ -274,11 +277,11 @@ class JobsRemoteInferenceHandler:
274
277
 
275
278
  job_in_queue = True
276
279
  while job_in_queue:
277
- result = self._attempt_fetch_job(
280
+ result, reason = self._attempt_fetch_job(
278
281
  job_info, remote_job_data_fetcher, object_fetcher
279
282
  )
280
283
  if result != "continue":
281
- return result
284
+ return result, reason
282
285
 
283
286
  async def create_and_poll_remote_job(
284
287
  self,
@@ -310,7 +313,7 @@ class JobsRemoteInferenceHandler:
310
313
  ),
311
314
  )
312
315
  if job_info is None:
313
- raise ValueError("Remote job creation failed.")
316
+ raise RemoteInferenceError("Remote job creation failed.")
314
317
 
315
318
  return await loop.run_in_executor(
316
319
  None,
@@ -1,8 +1,10 @@
1
- from typing import Optional, TYPE_CHECKING, Protocol
1
+ from typing import Protocol
2
2
  import sys
3
3
  #from edsl.scenarios.FileStore import HTMLFileStore
4
- from edsl.config import CONFIG
5
- from edsl.coop.coop import Coop
4
+ from ..config import CONFIG
5
+ from ..coop.coop import Coop
6
+ from ..scenarios import FileStore
7
+ from .exceptions import JobsErrors
6
8
 
7
9
 
8
10
  class ResultsProtocol(Protocol):
@@ -41,7 +43,8 @@ class ResultsExceptionsHandler:
41
43
  self.parameters = parameters
42
44
 
43
45
  self.open_in_browser = self._get_browser_setting()
44
- self.remote_logging = self._get_remote_logging_setting()
46
+ #self.remote_logging = self._get_remote_logging_setting()
47
+ self.remote_logging = False
45
48
 
46
49
  def _get_browser_setting(self) -> bool:
47
50
  """Determine if exceptions should be opened in browser based on config."""
@@ -51,7 +54,7 @@ class ResultsExceptionsHandler:
51
54
  elif setting == "False":
52
55
  return False
53
56
  else:
54
- raise Exception(
57
+ raise JobsErrors(
55
58
  "EDSL_OPEN_EXCEPTION_REPORT_URL must be either True or False"
56
59
  )
57
60
 
@@ -60,13 +63,13 @@ class ResultsExceptionsHandler:
60
63
  try:
61
64
  coop = Coop()
62
65
  return coop.edsl_settings["remote_logging"]
63
- except Exception as e:
66
+ except Exception:
64
67
  # print(e)
65
68
  return False
66
69
 
67
70
  def _generate_error_message(self, indices) -> str:
68
71
  """Generate appropriate error message based on number of exceptions."""
69
- msg = f"Exceptions were raised.\n"
72
+ msg = "Exceptions were raised.\n"
70
73
  return msg
71
74
 
72
75
  def handle_exceptions(self) -> None:
@@ -25,4 +25,6 @@ openai_key = keys['openai'].api_token
25
25
 
26
26
  from .key_lookup import KeyLookup
27
27
  from .key_lookup_collection import KeyLookupCollection
28
- from .key_lookup_builder import KeyLookupBuilder
28
+ from .key_lookup_builder import KeyLookupBuilder
29
+
30
+ __all__ = ["KeyLookup", "KeyLookupCollection", "KeyLookupBuilder"]
@@ -0,0 +1,62 @@
1
+ """
2
+ Exceptions specific to the key_management module.
3
+
4
+ This module defines custom exception classes for all key management-related errors
5
+ in the EDSL framework, ensuring consistent error handling and user feedback.
6
+ """
7
+
8
+ from ..base import BaseException
9
+
10
+
11
+ class KeyManagementError(BaseException):
12
+ """
13
+ Base exception class for all key management-related errors.
14
+
15
+ This is the parent class for all exceptions related to API key management,
16
+ including key lookup, validation, and configuration.
17
+
18
+ Examples:
19
+ ```python
20
+ # Usually not raised directly, but through subclasses
21
+ # For example, when attempting to use an invalid key
22
+ key_lookup.add_key(service="invalid_service", key="api_key") # Would raise KeyManagementServiceError
23
+ ```
24
+ """
25
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/api_keys.html"
26
+
27
+
28
+ class KeyManagementValueError(KeyManagementError):
29
+ """
30
+ Exception raised when invalid values are provided to key management operations.
31
+
32
+ This exception occurs when:
33
+ - Invalid fetch orders or configuration parameters are provided
34
+ - API key format is invalid
35
+ - Service names don't match expected values
36
+
37
+ Examples:
38
+ ```python
39
+ # Invalid fetch order
40
+ key_lookup_builder.set_fetch_order("invalid_order") # Raises KeyManagementValueError
41
+ ```
42
+ """
43
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/api_keys.html"
44
+
45
+
46
+ class KeyManagementDuplicateError(KeyManagementError):
47
+ """
48
+ Exception raised when duplicate keys or services are encountered.
49
+
50
+ This exception occurs when:
51
+ - Attempting to add a duplicate service ID
52
+ - Adding a key that already exists for a service
53
+ - Registering multiple handlers for the same service
54
+
55
+ Examples:
56
+ ```python
57
+ # Adding a duplicate service
58
+ builder.register_env_var(service_id="openai", env_var="OPENAI_API_KEY")
59
+ builder.register_env_var(service_id="openai", env_var="ANOTHER_KEY") # Raises KeyManagementDuplicateError
60
+ ```
61
+ """
62
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/api_keys.html"
@@ -19,7 +19,7 @@ class KeyLookup(UserDict):
19
19
  - Creating example instances for testing
20
20
 
21
21
  Typical usage:
22
- >>> from edsl.key_management.models import LanguageModelInput
22
+ >>> from .models import LanguageModelInput # Import for doctest
23
23
  >>> lookup = KeyLookup()
24
24
  >>> lookup['openai'] = LanguageModelInput(api_token='sk-key123', rpm=60, tpm=100000)
25
25
  >>> openai_config = lookup['openai']
@@ -1,9 +1,25 @@
1
- from typing import Optional, List
1
+ from typing import Optional, TYPE_CHECKING
2
2
  import os
3
3
  from functools import lru_cache
4
+ import textwrap
5
+
6
+ if TYPE_CHECKING:
7
+ from ..coop import Coop
4
8
 
5
9
  from ..enums import service_to_api_keyname
6
- from ..exceptions.general import MissingAPIKeyError
10
+ from ..base import BaseException
11
+
12
+ class MissingAPIKeyError(BaseException):
13
+ def __init__(self, full_message=None, model_name=None, inference_service=None, silent=False):
14
+ if model_name and inference_service:
15
+ full_message = textwrap.dedent(
16
+ f"""
17
+ An API Key for model `{model_name}` is missing from the .env file.
18
+ This key is associated with the inference service `{inference_service}`.
19
+ Please see https://docs.expectedparrot.com/en/latest/api_keys.html for more information.
20
+ """
21
+ )
22
+ super().__init__(full_message, show_docs=False, silent=silent)
7
23
 
8
24
  from .key_lookup import KeyLookup
9
25
  from .models import (
@@ -31,6 +47,8 @@ api_id_to_service = {"AWS_ACCESS_KEY_ID": "bedrock"}
31
47
  class KeyLookupBuilder:
32
48
  """Factory class for building KeyLookup objects by gathering credentials from multiple sources.
33
49
 
50
+ >>> from edsl.key_management.exceptions import KeyManagementValueError
51
+
34
52
  KeyLookupBuilder is responsible for discovering, organizing, and consolidating API keys
35
53
  and rate limits from various sources. It can pull credentials from:
36
54
 
@@ -67,9 +85,9 @@ class KeyLookupBuilder:
67
85
  Validation examples:
68
86
  >>> try:
69
87
  ... KeyLookupBuilder(fetch_order=["config", "env"]) # Should be tuple
70
- ... except ValueError as e:
71
- ... str(e)
72
- 'fetch_order must be a tuple'
88
+ ... except KeyManagementValueError as e:
89
+ ... "fetch_order must be a tuple" in str(e)
90
+ True
73
91
 
74
92
  >>> builder = KeyLookupBuilder()
75
93
  >>> builder.extract_service("EDSL_SERVICE_RPM_OPENAI")
@@ -93,7 +111,8 @@ class KeyLookupBuilder:
93
111
  fetch_order: Optional[tuple[str]] = None,
94
112
  coop: Optional["Coop"] = None,
95
113
  ):
96
- from ..coop import Coop
114
+ # Import here to avoid circular import issues
115
+ from ..coop import Coop # Import Coop type for type hinting
97
116
 
98
117
  # Fetch order goes from lowest priority to highest priority
99
118
  if fetch_order is None:
@@ -102,7 +121,8 @@ class KeyLookupBuilder:
102
121
  self.fetch_order = fetch_order
103
122
 
104
123
  if not isinstance(self.fetch_order, tuple):
105
- raise ValueError("fetch_order must be a tuple")
124
+ from edsl.key_management.exceptions import KeyManagementValueError
125
+ raise KeyManagementValueError("fetch_order must be a tuple")
106
126
 
107
127
  if coop is None:
108
128
  self.coop = Coop()
@@ -141,8 +161,8 @@ class KeyLookupBuilder:
141
161
  >>> lookup = builder.build()
142
162
  >>> isinstance(lookup, KeyLookup)
143
163
  True
144
- >>> lookup['test'].api_token # Test service should always exist
145
- 'test'
164
+ >>> lookup['test'].api_token == 'test' # Test service should always exist
165
+ True
146
166
 
147
167
  Technical Notes:
148
168
  - Skips services with missing API keys
@@ -194,7 +214,7 @@ class KeyLookupBuilder:
194
214
  - Supports services that require both API key and API ID
195
215
  """
196
216
  if (key_entries := self.key_data.get(service)) is None:
197
- raise MissingAPIKeyError(f"No key found for service '{service}'")
217
+ raise MissingAPIKeyError(f"No key found for service '{service}'", silent=True)
198
218
 
199
219
  if len(key_entries) == 1:
200
220
  api_key_entry = key_entries[0]
@@ -286,6 +306,8 @@ class KeyLookupBuilder:
286
306
 
287
307
  def _add_id(self, key: str, value: str, source: str) -> None:
288
308
  """Add an API ID to the id_data dictionary.
309
+
310
+ >>> from edsl.key_management.exceptions import KeyManagementDuplicateError
289
311
 
290
312
  >>> builder = KeyLookupBuilder()
291
313
  >>> builder._add_id("AWS_ACCESS_KEY_ID", "AKIA1234", "env")
@@ -293,9 +315,9 @@ class KeyLookupBuilder:
293
315
  'AKIA1234'
294
316
  >>> try:
295
317
  ... builder._add_id("AWS_ACCESS_KEY_ID", "AKIA5678", "env")
296
- ... except ValueError as e:
297
- ... str(e)
298
- 'Duplicate ID for service bedrock'
318
+ ... except KeyManagementDuplicateError as e:
319
+ ... "Duplicate ID for service bedrock" in str(e)
320
+ True
299
321
  """
300
322
  service = api_id_to_service[key]
301
323
  if service not in self.id_data:
@@ -303,7 +325,8 @@ class KeyLookupBuilder:
303
325
  service=service, name=key, value=value, source=source
304
326
  )
305
327
  else:
306
- raise ValueError(f"Duplicate ID for service {service}")
328
+ from edsl.key_management.exceptions import KeyManagementDuplicateError
329
+ raise KeyManagementDuplicateError(f"Duplicate ID for service {service}")
307
330
 
308
331
  def _add_limit(self, key: str, value: str, source: str) -> None:
309
332
  """Add a rate limit entry to the limit_data dictionary.
@@ -1,5 +1,6 @@
1
1
  from collections import UserDict
2
2
 
3
+ # Import for doctest and type hints
3
4
  from .key_lookup_builder import KeyLookupBuilder
4
5
  from .key_lookup import KeyLookup
5
6
 
@@ -25,6 +26,7 @@ class KeyLookupCollection(UserDict):
25
26
  True
26
27
 
27
28
  Basic usage:
29
+ >>> from edsl.key_management import KeyLookup
28
30
  >>> collection = KeyLookupCollection()
29
31
  >>> collection.add_key_lookup(("config", "env"))
30
32
  >>> lookup = collection[("config", "env")] # Get the stored KeyLookup
@@ -4,4 +4,4 @@ from .model_list import ModelList
4
4
 
5
5
  from .exceptions import LanguageModelBadResponseError
6
6
 
7
- __all__ = ["Model", "ModelList", "LanguageModelBadResponseError"]
7
+ __all__ = ["Model", "ModelList", "LanguageModelBadResponseError", "LanguageModel"]