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
edsl/jobs/exceptions.py CHANGED
@@ -1,26 +1,195 @@
1
- from textwrap import dedent
2
1
 
3
2
  from ..base import BaseException
4
3
 
5
4
  class JobsErrors(BaseException):
6
- pass
5
+ """
6
+ Base exception class for all job-related errors.
7
+
8
+ This is the parent class for all exceptions related to job execution
9
+ in the EDSL framework. It provides a common type for catching any
10
+ job-specific error.
11
+ """
12
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/jobs.html"
7
13
 
8
14
 
9
15
  class JobsRunError(JobsErrors):
10
- pass
16
+ """
17
+ Exception raised when a job fails to run correctly.
18
+
19
+ This exception indicates issues with job execution such as:
20
+ - Configuration problems before job execution
21
+ - Resource allocation failures
22
+ - Job termination due to internal errors
23
+
24
+ To fix this error:
25
+ 1. Check the job configuration for any invalid parameters
26
+ 2. Ensure all required resources (models, agents, etc.) are available
27
+ 3. Verify that dependent services are accessible
28
+
29
+ Note: This exception is currently not used actively in the codebase,
30
+ but is kept for potential future use and test cases.
31
+ """
32
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/jobs.html"
11
33
 
12
34
 
13
35
  class MissingRemoteInferenceError(JobsErrors):
14
- pass
36
+ """
37
+ Exception raised when remote inference is required but not configured.
38
+
39
+ This exception occurs when:
40
+ - A job requires remote inference capabilities but they're not available
41
+ - Credentials for remote inference are missing or invalid
42
+
43
+ To fix this error:
44
+ 1. Set up remote inference configuration in your environment
45
+ 2. Provide valid API keys for the required inference service
46
+
47
+ Note: This exception is defined but not currently used in the codebase.
48
+ It raises Exception("not used") to indicate this state.
49
+ """
50
+ def __init__(self, message="Remote inference configuration is missing", **kwargs):
51
+ super().__init__(message, **kwargs)
15
52
 
16
53
 
17
- class InterviewError(Exception):
18
- pass
54
+ class InterviewError(JobsErrors):
55
+ """
56
+ Base exception class for all interview-related errors.
57
+
58
+ This exception serves as the parent class for specific interview errors
59
+ and handles cases where an interview process fails for reasons not covered
60
+ by more specific exceptions.
61
+ """
62
+
63
+ def __init__(self, message="An error occurred during the interview process", **kwargs):
64
+ super().__init__(message, **kwargs)
65
+ self.message = message
19
66
 
20
67
 
21
68
  class InterviewErrorPriorTaskCanceled(InterviewError):
22
- pass
69
+ """
70
+ Exception raised when a task cannot run because a dependent task failed.
71
+
72
+ This exception is raised in task pipelines when a prerequisite task
73
+ fails or is canceled, preventing dependent tasks from executing.
74
+
75
+ When you encounter this error:
76
+ 1. Check the error logs for information about the failed dependent task
77
+ 2. Fix any issues with the prerequisite task first before retrying
78
+ 3. If using custom task dependencies, verify that the dependency chain is correct
79
+ """
80
+
81
+ def __init__(self, message="Cannot run this task because a required prior task was canceled or failed"):
82
+ super().__init__(message)
23
83
 
24
84
 
25
85
  class InterviewTimeoutError(InterviewError):
26
- pass
86
+ """
87
+ Exception raised when an interview operation times out.
88
+
89
+ This exception indicates that a model call or other operation
90
+ during an interview process took too long to complete.
91
+
92
+ To fix this error:
93
+ 1. Check your network connection if using remote models
94
+ 2. Consider increasing timeout settings if dealing with complex prompts
95
+ 3. Try a different model provider if consistently experiencing timeouts
96
+
97
+ Note: While defined here, the codebase currently uses LanguageModelNoResponseError
98
+ to handle timeouts in actual operation.
99
+ """
100
+
101
+ def __init__(self, message="The interview operation timed out", **kwargs):
102
+ super().__init__(message, **kwargs)
103
+
104
+
105
+ class JobsValueError(JobsErrors):
106
+ """
107
+ Exception raised when there's an invalid value in job-related operations.
108
+
109
+ This exception indicates that a parameter or value used in job configuration
110
+ or execution is invalid, out of range, or otherwise inappropriate.
111
+
112
+ Common causes include:
113
+ - Invalid question names in job configuration
114
+ - Incompatible survey and scenario combinations
115
+ - Invalid parameter values for job construction
116
+
117
+ To fix this error:
118
+ 1. Check the parameter values in your job configuration
119
+ 2. Ensure that all question names exist in the survey
120
+ 3. Verify that survey and scenario combinations are compatible
121
+ """
122
+
123
+ def __init__(self, message="Invalid value in jobs module", **kwargs):
124
+ super().__init__(message, **kwargs)
125
+
126
+
127
+ class JobsCompatibilityError(JobsErrors):
128
+ """
129
+ Exception raised when there are compatibility issues between components.
130
+
131
+ This exception indicates that the components being used together (like
132
+ surveys and scenarios) are not compatible with each other for the requested
133
+ operation.
134
+
135
+ To fix this error:
136
+ 1. Check that your survey and scenario are compatible
137
+ 2. Ensure all referenced questions exist in the survey
138
+ 3. Verify that scenario fields match expected inputs for questions
139
+ """
140
+
141
+ def __init__(self, message="Compatibility issue between job components", **kwargs):
142
+ super().__init__(message, **kwargs)
143
+
144
+
145
+ class JobsImplementationError(JobsErrors):
146
+ """
147
+ Exception raised when a required method or feature is not implemented.
148
+
149
+ This exception indicates that a method or functionality expected to be
150
+ available is not implemented, typically in abstract classes or
151
+ interfaces that require concrete implementation.
152
+
153
+ To fix this error:
154
+ 1. Implement the required method in your subclass
155
+ 2. Use a different implementation that provides the required functionality
156
+ 3. Check for updates to the library that might implement this feature
157
+ """
158
+
159
+ def __init__(self, message="Required method or feature is not implemented", **kwargs):
160
+ super().__init__(message, **kwargs)
161
+
162
+
163
+ class RemoteInferenceError(JobsErrors):
164
+ """
165
+ Exception raised when there are issues with remote inference.
166
+
167
+ This exception indicates problems with remote inference configuration,
168
+ connection, or execution. It can occur when a remote job fails to create
169
+ or execute properly.
170
+
171
+ To fix this error:
172
+ 1. Check your remote inference configuration
173
+ 2. Verify API keys and authentication are correct
174
+ 3. Ensure the remote service is available and responsive
175
+ """
176
+
177
+ def __init__(self, message="Remote inference operation failed", **kwargs):
178
+ super().__init__(message, **kwargs)
179
+
180
+
181
+ class JobsTypeError(JobsErrors):
182
+ """
183
+ Exception raised when there's a type mismatch in job-related operations.
184
+
185
+ This exception indicates that a parameter or value is of the wrong type
186
+ for the operation being performed.
187
+
188
+ To fix this error:
189
+ 1. Check the types of parameters you're passing to job functions
190
+ 2. Ensure that you're using the correct types as defined in the API
191
+ 3. Convert parameters to the expected types if necessary
192
+ """
193
+
194
+ def __init__(self, message="Type mismatch in jobs module", **kwargs):
195
+ super().__init__(message, **kwargs)
@@ -1,4 +1,4 @@
1
- from typing import List, Dict, Any, Optional, TYPE_CHECKING
1
+ from typing import Dict, Any, Optional, TYPE_CHECKING
2
2
 
3
3
  if TYPE_CHECKING:
4
4
  from ..questions import QuestionBase
edsl/jobs/jobs.py CHANGED
@@ -17,19 +17,14 @@ who need to run complex simulations with language models.
17
17
  """
18
18
  from __future__ import annotations
19
19
  import asyncio
20
- from inspect import signature
21
20
  from typing import Optional, Union, TypeVar, Callable, cast
22
21
  from functools import wraps
23
22
 
24
23
  from typing import (
25
24
  Literal,
26
- Optional,
27
- Union,
28
25
  Sequence,
29
26
  Generator,
30
27
  TYPE_CHECKING,
31
- Callable,
32
- Tuple,
33
28
  )
34
29
 
35
30
  from ..base import Base
@@ -40,6 +35,7 @@ from ..buckets import BucketCollection
40
35
  from ..scenarios import Scenario, ScenarioList
41
36
  from ..surveys import Survey
42
37
  from ..interviews import Interview
38
+ from .exceptions import JobsValueError, JobsImplementationError
43
39
 
44
40
  from .jobs_pricing_estimation import JobsPrompts
45
41
  from .remote_inference import JobsRemoteInferenceHandler
@@ -73,27 +69,25 @@ P = ParamSpec("P")
73
69
  T = TypeVar("T")
74
70
 
75
71
 
76
-
77
-
78
72
  def with_config(f: Callable[P, T]) -> Callable[P, T]:
79
73
  """
80
74
  Decorator that processes function parameters to match the RunConfig dataclass structure.
81
-
75
+
82
76
  This decorator is used primarily with the run() and run_async() methods to provide
83
77
  a consistent interface for job configuration while maintaining a clean API.
84
-
78
+
85
79
  The decorator:
86
80
  1. Extracts environment-related parameters into a RunEnvironment instance
87
81
  2. Extracts execution-related parameters into a RunParameters instance
88
82
  3. Combines both into a single RunConfig object
89
83
  4. Passes this RunConfig to the decorated function as a keyword argument
90
-
84
+
91
85
  Parameters:
92
86
  f (Callable): The function to decorate, typically run() or run_async()
93
-
87
+
94
88
  Returns:
95
89
  Callable: A wrapped function that accepts all RunConfig parameters directly
96
-
90
+
97
91
  Example:
98
92
  @with_config
99
93
  def run(self, *, config: RunConfig) -> Results:
@@ -107,7 +101,8 @@ def with_config(f: Callable[P, T]) -> Callable[P, T]:
107
101
  name: field.default
108
102
  for name, field in RunEnvironment.__dataclass_fields__.items()
109
103
  }
110
- combined = {**parameter_fields, **environment_fields}
104
+ # Combined fields dict used for reference during development
105
+ # combined = {**parameter_fields, **environment_fields}
111
106
 
112
107
  @wraps(f)
113
108
  def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
@@ -126,25 +121,25 @@ def with_config(f: Callable[P, T]) -> Callable[P, T]:
126
121
  class Jobs(Base):
127
122
  """
128
123
  A collection of agents, scenarios, models, and a survey that orchestrates interviews.
129
-
124
+
130
125
  The Jobs class is the central component for running large-scale experiments or simulations
131
126
  in EDSL. It manages the execution of interviews where agents interact with surveys through
132
127
  language models, possibly in different scenarios.
133
-
128
+
134
129
  Key responsibilities:
135
130
  1. Managing collections of agents, scenarios, and models
136
131
  2. Configuring execution parameters (caching, API keys, etc.)
137
132
  3. Managing parallel execution of interviews
138
133
  4. Handling remote cache and inference capabilities
139
134
  5. Collecting and organizing results
140
-
135
+
141
136
  A typical workflow involves:
142
137
  1. Creating a survey with questions
143
138
  2. Creating a Jobs instance with that survey
144
139
  3. Adding agents, scenarios, and models using the `by()` method
145
140
  4. Running the job with `run()` or `run_async()`
146
141
  5. Analyzing the results
147
-
142
+
148
143
  Jobs implements a fluent interface pattern, where methods return self to allow
149
144
  method chaining for concise, readable configuration.
150
145
  """
@@ -159,22 +154,22 @@ class Jobs(Base):
159
154
  scenarios: Optional[Union["ScenarioList", list["Scenario"]]] = None,
160
155
  ):
161
156
  """Initialize a Jobs instance with a survey and optional components.
162
-
157
+
163
158
  The Jobs constructor requires a survey and optionally accepts collections of
164
159
  agents, models, and scenarios. If any of these optional components are not provided,
165
160
  they can be added later using the `by()` method or will be automatically populated
166
161
  with defaults when the job is run.
167
-
162
+
168
163
  Parameters:
169
164
  survey (Survey): The survey containing questions to be used in the job
170
165
  agents (Union[list[Agent], AgentList], optional): The agents that will take the survey
171
166
  models (Union[ModelList, list[LanguageModel]], optional): The language models to use
172
167
  scenarios (Union[ScenarioList, list[Scenario]], optional): The scenarios to run
173
-
168
+
174
169
  Raises:
175
170
  ValueError: If the survey contains questions with invalid names
176
171
  (e.g., names containing template variables)
177
-
172
+
178
173
  Examples:
179
174
  >>> from edsl.surveys import Survey
180
175
  >>> from edsl.questions import QuestionFreeText
@@ -183,11 +178,7 @@ class Jobs(Base):
183
178
  >>> j = Jobs(survey = s)
184
179
  >>> q = QuestionFreeText(question_name="{{ bad_name }}", question_text="What is your name?")
185
180
  >>> s = Survey(questions=[q])
186
- >>> j = Jobs(survey = s)
187
- Traceback (most recent call last):
188
- ...
189
- ValueError: At least some question names are not valid: ['{{ bad_name }}']
190
-
181
+
191
182
  Notes:
192
183
  - The survey's questions must have valid names without templating variables
193
184
  - If agents, models, or scenarios are not provided, defaults will be used when running
@@ -206,10 +197,15 @@ class Jobs(Base):
206
197
 
207
198
  try:
208
199
  assert self.survey.question_names_valid()
209
- except Exception as e:
210
- invalid_question_names = [q.question_name for q in self.survey.questions if not q.is_valid_question_name()]
211
- raise ValueError(f"At least some question names are not valid: {invalid_question_names}")
212
-
200
+ except Exception:
201
+ invalid_question_names = [
202
+ q.question_name
203
+ for q in self.survey.questions
204
+ if not q.is_valid_question_name()
205
+ ]
206
+ raise JobsValueError(
207
+ f"At least some question names are not valid: {invalid_question_names}"
208
+ )
213
209
 
214
210
  def add_running_env(self, running_env: RunEnvironment):
215
211
  self.run_config.add_environment(running_env)
@@ -224,7 +220,7 @@ class Jobs(Base):
224
220
  self.run_config.add_cache(cache)
225
221
  return self
226
222
 
227
- def using_bucket_collection(self, bucket_collection: 'BucketCollection') -> Jobs:
223
+ def using_bucket_collection(self, bucket_collection: "BucketCollection") -> Jobs:
228
224
  """
229
225
  Add a BucketCollection to the job.
230
226
 
@@ -233,7 +229,7 @@ class Jobs(Base):
233
229
  self.run_config.add_bucket_collection(bucket_collection)
234
230
  return self
235
231
 
236
- def using_key_lookup(self, key_lookup: 'KeyLookup') -> Jobs:
232
+ def using_key_lookup(self, key_lookup: "KeyLookup") -> Jobs:
237
233
  """
238
234
  Add a KeyLookup to the job.
239
235
 
@@ -339,18 +335,18 @@ class Jobs(Base):
339
335
  ) -> "Jobs":
340
336
  """
341
337
  Add agents, scenarios, and language models to a job using a fluent interface.
342
-
338
+
343
339
  This method is the primary way to configure a Jobs instance with components.
344
340
  It intelligently handles different types of objects and collections, making
345
341
  it easy to build complex job configurations with a concise syntax.
346
-
342
+
347
343
  Parameters:
348
- *args: Objects or sequences of objects to add to the job.
344
+ *args: Objects or sequences of objects to add to the job.
349
345
  Supported types are Agent, Scenario, LanguageModel, and sequences of these.
350
-
346
+
351
347
  Returns:
352
348
  Jobs: The Jobs instance (self) for method chaining
353
-
349
+
354
350
  Examples:
355
351
  >>> from edsl.surveys import Survey
356
352
  >>> from edsl.questions import QuestionFreeText
@@ -361,17 +357,17 @@ class Jobs(Base):
361
357
  >>> from edsl.agents import Agent; a = Agent(traits = {"status": "Sad"})
362
358
  >>> j.by(a).agents
363
359
  AgentList([Agent(traits = {'status': 'Sad'})])
364
-
360
+
365
361
  # Adding multiple components at once
366
362
  >>> from edsl.language_models import Model
367
363
  >>> from edsl.scenarios import Scenario
368
364
  >>> j = Jobs.example()
369
365
  >>> _ = j.by(Agent(traits={"mood": "happy"})).by(Model(temperature=0.7)).by(Scenario({"time": "morning"}))
370
-
366
+
371
367
  # Adding a sequence of the same type
372
368
  >>> agents = [Agent(traits={"age": i}) for i in range(5)]
373
369
  >>> _ = j.by(agents)
374
-
370
+
375
371
  Notes:
376
372
  - All objects must implement 'get_value', 'set_value', and '__add__' methods
377
373
  - Agent traits: When adding agents with traits to existing agents, the traits are
@@ -475,16 +471,19 @@ class Jobs(Base):
475
471
 
476
472
  def show_flow(self, filename: Optional[str] = None) -> None:
477
473
  """Show the flow of the survey.
478
-
474
+
479
475
  >>> from edsl.jobs import Jobs
480
476
  >>> Jobs.example().show_flow()
481
477
  """
482
478
  from ..surveys import SurveyFlowVisualization
483
- if self.scenarios:
479
+
480
+ if self.scenarios:
484
481
  scenario = self.scenarios[0]
485
482
  else:
486
483
  scenario = None
487
- SurveyFlowVisualization(self.survey, scenario=scenario, agent=None).show_flow(filename=filename)
484
+ SurveyFlowVisualization(self.survey, scenario=scenario, agent=None).show_flow(
485
+ filename=filename
486
+ )
488
487
 
489
488
  def interviews(self) -> list[Interview]:
490
489
  """
@@ -585,7 +584,7 @@ class Jobs(Base):
585
584
  return user_edsl_settings.get("remote_caching", False)
586
585
  except requests.ConnectionError:
587
586
  pass
588
- except CoopServerResponseError as e:
587
+ except CoopServerResponseError:
589
588
  pass
590
589
 
591
590
  return False
@@ -604,7 +603,7 @@ class Jobs(Base):
604
603
  )
605
604
  return job_info
606
605
 
607
- def _create_remote_inference_handler(self) -> 'JobsRemoteInferenceHandler':
606
+ def _create_remote_inference_handler(self) -> "JobsRemoteInferenceHandler":
608
607
  return JobsRemoteInferenceHandler(
609
608
  self, verbose=self.run_config.parameters.verbose
610
609
  )
@@ -621,15 +620,15 @@ class Jobs(Base):
621
620
  if jh.use_remote_inference(self.run_config.parameters.disable_remote_inference):
622
621
  job_info: RemoteJobInfo = self._start_remote_inference_job(jh)
623
622
  if background:
624
- from edsl.results import Results
623
+ from ..results import Results
625
624
 
626
625
  results = Results.from_job_info(job_info)
627
- return results
626
+ return results, None
628
627
  else:
629
- results = jh.poll_remote_inference_job(job_info)
630
- return results
628
+ results, reason = jh.poll_remote_inference_job(job_info)
629
+ return results, reason
631
630
  else:
632
- return None
631
+ return None, None
633
632
 
634
633
  def _prepare_to_run(self) -> None:
635
634
  "This makes sure that the job is ready to run and that keys are in place for a remote job."
@@ -646,9 +645,9 @@ class Jobs(Base):
646
645
  jc.check_api_keys()
647
646
 
648
647
  async def _execute_with_remote_cache(self, run_job_async: bool) -> Results:
649
- use_remote_cache = self.use_remote_cache()
648
+ # Remote cache usage determination happens inside this method
649
+ # use_remote_cache = self.use_remote_cache()
650
650
 
651
- from ..coop import Coop
652
651
  from .jobs_runner_asyncio import JobsRunnerAsyncio
653
652
  from ..caching import Cache
654
653
 
@@ -707,8 +706,9 @@ class Jobs(Base):
707
706
  self.run_config.environment.cache = Cache(immediate_write=False)
708
707
 
709
708
  # first try to run the job remotely
710
- if (results := self._remote_results(config)) is not None:
711
- return results
709
+ results, reason = self._remote_results(config)
710
+ if results is not None:
711
+ return results, reason
712
712
 
713
713
  self._check_if_local_keys_ok()
714
714
 
@@ -725,17 +725,17 @@ class Jobs(Base):
725
725
  self.run_config.environment.key_lookup
726
726
  )
727
727
 
728
- return None
728
+ return None, reason
729
729
 
730
730
  @with_config
731
731
  def run(self, *, config: RunConfig) -> "Results":
732
732
  """
733
733
  Runs the job by conducting interviews and returns their results.
734
-
734
+
735
735
  This is the main entry point for executing a job. It processes all interviews
736
736
  (combinations of agents, scenarios, and models) and returns a Results object
737
737
  containing all responses and metadata.
738
-
738
+
739
739
  Parameters:
740
740
  n (int): Number of iterations to run each interview (default: 1)
741
741
  progress_bar (bool): Whether to show a progress bar (default: False)
@@ -756,16 +756,16 @@ class Jobs(Base):
756
756
  cache (Cache, optional): Cache object to store results
757
757
  bucket_collection (BucketCollection, optional): Object to track API calls
758
758
  key_lookup (KeyLookup, optional): Object to manage API keys
759
-
759
+
760
760
  Returns:
761
761
  Results: A Results object containing all responses and metadata
762
-
762
+
763
763
  Notes:
764
764
  - This method will first try to use remote inference if available
765
765
  - If remote inference is not available, it will run locally
766
766
  - For long-running jobs, consider using progress_bar=True
767
767
  - For maximum performance, ensure appropriate caching is configured
768
-
768
+
769
769
  Example:
770
770
  >>> from edsl.jobs import Jobs
771
771
  >>> from edsl.caching import Cache
@@ -775,22 +775,25 @@ class Jobs(Base):
775
775
  >>> results = job.by(m).run(cache=Cache(), progress_bar=False, n=2, disable_remote_inference=True)
776
776
  ...
777
777
  """
778
- potentially_completed_results = self._run(config)
778
+ potentially_completed_results, reason = self._run(config)
779
779
 
780
780
  if potentially_completed_results is not None:
781
781
  return potentially_completed_results
782
782
 
783
+ if reason == "insufficient funds":
784
+ return None
785
+
783
786
  return asyncio.run(self._execute_with_remote_cache(run_job_async=False))
784
787
 
785
788
  @with_config
786
789
  async def run_async(self, *, config: RunConfig) -> "Results":
787
790
  """
788
791
  Asynchronously runs the job by conducting interviews and returns their results.
789
-
792
+
790
793
  This method is the asynchronous version of `run()`. It has the same functionality and
791
794
  parameters but can be awaited in an async context for better integration with
792
795
  asynchronous code.
793
-
796
+
794
797
  Parameters:
795
798
  n (int): Number of iterations to run each interview (default: 1)
796
799
  progress_bar (bool): Whether to show a progress bar (default: False)
@@ -811,15 +814,15 @@ class Jobs(Base):
811
814
  cache (Cache, optional): Cache object to store results
812
815
  bucket_collection (BucketCollection, optional): Object to track API calls
813
816
  key_lookup (KeyLookup, optional): Object to manage API keys
814
-
817
+
815
818
  Returns:
816
819
  Results: A Results object containing all responses and metadata
817
-
820
+
818
821
  Notes:
819
822
  - This method should be used in async contexts (e.g., with `await`)
820
823
  - For non-async contexts, use the `run()` method instead
821
824
  - This method is particularly useful in notebook environments or async applications
822
-
825
+
823
826
  Example:
824
827
  >>> import asyncio
825
828
  >>> from edsl.jobs import Jobs
@@ -878,7 +881,7 @@ class Jobs(Base):
878
881
  ],
879
882
  }
880
883
  if add_edsl_version:
881
- from edsl import __version__
884
+ from .. import __version__
882
885
 
883
886
  d["edsl_version"] = __version__
884
887
  d["edsl_class_name"] = "Jobs"
@@ -960,7 +963,9 @@ class Jobs(Base):
960
963
  """Return the answer to a question. This is a method that can be added to an agent."""
961
964
 
962
965
  if random.random() < throw_exception_probability:
963
- raise Exception("Error!")
966
+ from .exceptions import JobsErrors
967
+
968
+ raise JobsErrors("Simulated error during question answering")
964
969
  return agent_answers[
965
970
  (self.traits["status"], question.question_name, scenario["period"])
966
971
  ]
@@ -1001,7 +1006,7 @@ class Jobs(Base):
1001
1006
 
1002
1007
  def code(self):
1003
1008
  """Return the code to create this instance."""
1004
- raise NotImplementedError
1009
+ raise JobsImplementationError("Code generation not implemented yet")
1005
1010
 
1006
1011
 
1007
1012
  def main():
edsl/jobs/jobs_checks.py CHANGED
@@ -3,7 +3,7 @@ Checks a Jobs object for missing API keys and other requirements.
3
3
  """
4
4
 
5
5
  import os
6
- from edsl.exceptions.general import MissingAPIKeyError
6
+ from ..key_management.key_lookup_builder import MissingAPIKeyError
7
7
 
8
8
 
9
9
  class JobsChecks:
@@ -16,7 +16,7 @@ class JobsChecks:
16
16
  self.jobs = jobs
17
17
 
18
18
  def check_api_keys(self) -> None:
19
- from edsl.language_models.model import Model
19
+ from ..language_models.model import Model
20
20
 
21
21
  if len(self.jobs.models) == 0:
22
22
  models = [Model()]
@@ -28,6 +28,7 @@ class JobsChecks:
28
28
  raise MissingAPIKeyError(
29
29
  model_name=str(model.model),
30
30
  inference_service=model._inference_service_,
31
+ silent=False
31
32
  )
32
33
 
33
34
  def get_missing_api_keys(self) -> set:
@@ -36,8 +37,7 @@ class JobsChecks:
36
37
  """
37
38
  missing_api_keys = set()
38
39
 
39
- from edsl.language_models.model import Model
40
- from edsl.enums import service_to_api_keyname
40
+ from ..enums import service_to_api_keyname
41
41
 
42
42
  for model in self.jobs.models: # + [Model()]:
43
43
  if not model.has_valid_api_key():
@@ -134,9 +134,8 @@ class JobsChecks:
134
134
  def key_process(self):
135
135
  import secrets
136
136
  from dotenv import load_dotenv
137
- from edsl.config import CONFIG
138
- from edsl.coop.coop import Coop
139
- from edsl.utilities.utilities import write_api_key_to_env
137
+ from ..coop.coop import Coop
138
+ from ..utilities.utilities import write_api_key_to_env
140
139
 
141
140
  missing_api_keys = self.get_missing_api_keys()
142
141