edsl 0.1.39__py3-none-any.whl → 0.1.39.dev1__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 (212) hide show
  1. edsl/Base.py +116 -197
  2. edsl/__init__.py +7 -15
  3. edsl/__version__.py +1 -1
  4. edsl/agents/Agent.py +147 -351
  5. edsl/agents/AgentList.py +73 -211
  6. edsl/agents/Invigilator.py +50 -101
  7. edsl/agents/InvigilatorBase.py +70 -62
  8. edsl/agents/PromptConstructor.py +225 -143
  9. edsl/agents/__init__.py +1 -0
  10. edsl/agents/prompt_helpers.py +3 -3
  11. edsl/auto/AutoStudy.py +5 -18
  12. edsl/auto/StageBase.py +40 -53
  13. edsl/auto/StageQuestions.py +1 -2
  14. edsl/auto/utilities.py +6 -0
  15. edsl/config.py +2 -22
  16. edsl/conversation/car_buying.py +1 -2
  17. edsl/coop/PriceFetcher.py +1 -1
  18. edsl/coop/coop.py +47 -125
  19. edsl/coop/utils.py +14 -14
  20. edsl/data/Cache.py +27 -45
  21. edsl/data/CacheEntry.py +15 -12
  22. edsl/data/CacheHandler.py +12 -31
  23. edsl/data/RemoteCacheSync.py +46 -154
  24. edsl/data/__init__.py +3 -4
  25. edsl/data_transfer_models.py +1 -2
  26. edsl/enums.py +0 -27
  27. edsl/exceptions/__init__.py +50 -50
  28. edsl/exceptions/agents.py +0 -12
  29. edsl/exceptions/questions.py +6 -24
  30. edsl/exceptions/scenarios.py +0 -7
  31. edsl/inference_services/AnthropicService.py +19 -38
  32. edsl/inference_services/AwsBedrock.py +2 -0
  33. edsl/inference_services/AzureAI.py +2 -0
  34. edsl/inference_services/GoogleService.py +12 -7
  35. edsl/inference_services/InferenceServiceABC.py +85 -18
  36. edsl/inference_services/InferenceServicesCollection.py +79 -120
  37. edsl/inference_services/MistralAIService.py +3 -0
  38. edsl/inference_services/OpenAIService.py +35 -47
  39. edsl/inference_services/PerplexityService.py +3 -0
  40. edsl/inference_services/TestService.py +10 -11
  41. edsl/inference_services/TogetherAIService.py +3 -5
  42. edsl/jobs/Answers.py +14 -1
  43. edsl/jobs/Jobs.py +431 -356
  44. edsl/jobs/JobsChecks.py +10 -35
  45. edsl/jobs/JobsPrompts.py +4 -6
  46. edsl/jobs/JobsRemoteInferenceHandler.py +133 -205
  47. edsl/jobs/buckets/BucketCollection.py +3 -44
  48. edsl/jobs/buckets/TokenBucket.py +21 -53
  49. edsl/jobs/interviews/Interview.py +408 -143
  50. edsl/jobs/runners/JobsRunnerAsyncio.py +403 -88
  51. edsl/jobs/runners/JobsRunnerStatus.py +165 -133
  52. edsl/jobs/tasks/QuestionTaskCreator.py +19 -21
  53. edsl/jobs/tasks/TaskHistory.py +18 -38
  54. edsl/jobs/tasks/task_status_enum.py +2 -0
  55. edsl/language_models/KeyLookup.py +30 -0
  56. edsl/language_models/LanguageModel.py +236 -194
  57. edsl/language_models/ModelList.py +19 -28
  58. edsl/language_models/__init__.py +2 -1
  59. edsl/language_models/registry.py +190 -0
  60. edsl/language_models/repair.py +2 -2
  61. edsl/language_models/unused/ReplicateBase.py +83 -0
  62. edsl/language_models/utilities.py +4 -5
  63. edsl/notebooks/Notebook.py +14 -19
  64. edsl/prompts/Prompt.py +39 -29
  65. edsl/questions/{answer_validator_mixin.py → AnswerValidatorMixin.py} +2 -47
  66. edsl/questions/QuestionBase.py +214 -68
  67. edsl/questions/{question_base_gen_mixin.py → QuestionBaseGenMixin.py} +50 -57
  68. edsl/questions/QuestionBasePromptsMixin.py +3 -7
  69. edsl/questions/QuestionBudget.py +1 -1
  70. edsl/questions/QuestionCheckBox.py +3 -3
  71. edsl/questions/QuestionExtract.py +7 -5
  72. edsl/questions/QuestionFreeText.py +3 -2
  73. edsl/questions/QuestionList.py +18 -10
  74. edsl/questions/QuestionMultipleChoice.py +23 -67
  75. edsl/questions/QuestionNumerical.py +4 -2
  76. edsl/questions/QuestionRank.py +17 -7
  77. edsl/questions/{response_validator_abc.py → ResponseValidatorABC.py} +26 -40
  78. edsl/questions/SimpleAskMixin.py +3 -4
  79. edsl/questions/__init__.py +1 -2
  80. edsl/questions/derived/QuestionLinearScale.py +3 -6
  81. edsl/questions/derived/QuestionTopK.py +1 -1
  82. edsl/questions/descriptors.py +3 -17
  83. edsl/questions/question_registry.py +1 -1
  84. edsl/results/CSSParameterizer.py +1 -1
  85. edsl/results/Dataset.py +7 -170
  86. edsl/results/DatasetExportMixin.py +305 -168
  87. edsl/results/DatasetTree.py +8 -28
  88. edsl/results/Result.py +206 -298
  89. edsl/results/Results.py +131 -149
  90. edsl/results/ResultsDBMixin.py +238 -0
  91. edsl/results/ResultsExportMixin.py +0 -2
  92. edsl/results/{results_selector.py → Selector.py} +13 -23
  93. edsl/results/TableDisplay.py +171 -98
  94. edsl/results/__init__.py +1 -1
  95. edsl/scenarios/FileStore.py +239 -150
  96. edsl/scenarios/Scenario.py +193 -90
  97. edsl/scenarios/ScenarioHtmlMixin.py +3 -4
  98. edsl/scenarios/{scenario_join.py → ScenarioJoin.py} +6 -10
  99. edsl/scenarios/ScenarioList.py +244 -415
  100. edsl/scenarios/ScenarioListExportMixin.py +7 -0
  101. edsl/scenarios/ScenarioListPdfMixin.py +37 -15
  102. edsl/scenarios/__init__.py +2 -1
  103. edsl/study/ObjectEntry.py +1 -1
  104. edsl/study/SnapShot.py +1 -1
  105. edsl/study/Study.py +12 -5
  106. edsl/surveys/Rule.py +4 -5
  107. edsl/surveys/RuleCollection.py +27 -25
  108. edsl/surveys/Survey.py +791 -270
  109. edsl/surveys/SurveyCSS.py +8 -20
  110. edsl/surveys/{SurveyFlowVisualization.py → SurveyFlowVisualizationMixin.py} +9 -11
  111. edsl/surveys/__init__.py +2 -4
  112. edsl/surveys/descriptors.py +2 -6
  113. edsl/surveys/instructions/ChangeInstruction.py +2 -1
  114. edsl/surveys/instructions/Instruction.py +13 -4
  115. edsl/surveys/instructions/InstructionCollection.py +6 -11
  116. edsl/templates/error_reporting/interview_details.html +1 -1
  117. edsl/templates/error_reporting/report.html +1 -1
  118. edsl/tools/plotting.py +1 -1
  119. edsl/utilities/utilities.py +23 -35
  120. {edsl-0.1.39.dist-info → edsl-0.1.39.dev1.dist-info}/METADATA +10 -12
  121. edsl-0.1.39.dev1.dist-info/RECORD +277 -0
  122. {edsl-0.1.39.dist-info → edsl-0.1.39.dev1.dist-info}/WHEEL +1 -1
  123. edsl/agents/QuestionInstructionPromptBuilder.py +0 -128
  124. edsl/agents/QuestionTemplateReplacementsBuilder.py +0 -137
  125. edsl/agents/question_option_processor.py +0 -172
  126. edsl/coop/CoopFunctionsMixin.py +0 -15
  127. edsl/coop/ExpectedParrotKeyHandler.py +0 -125
  128. edsl/exceptions/inference_services.py +0 -5
  129. edsl/inference_services/AvailableModelCacheHandler.py +0 -184
  130. edsl/inference_services/AvailableModelFetcher.py +0 -215
  131. edsl/inference_services/ServiceAvailability.py +0 -135
  132. edsl/inference_services/data_structures.py +0 -134
  133. edsl/jobs/AnswerQuestionFunctionConstructor.py +0 -223
  134. edsl/jobs/FetchInvigilator.py +0 -47
  135. edsl/jobs/InterviewTaskManager.py +0 -98
  136. edsl/jobs/InterviewsConstructor.py +0 -50
  137. edsl/jobs/JobsComponentConstructor.py +0 -189
  138. edsl/jobs/JobsRemoteInferenceLogger.py +0 -239
  139. edsl/jobs/RequestTokenEstimator.py +0 -30
  140. edsl/jobs/async_interview_runner.py +0 -138
  141. edsl/jobs/buckets/TokenBucketAPI.py +0 -211
  142. edsl/jobs/buckets/TokenBucketClient.py +0 -191
  143. edsl/jobs/check_survey_scenario_compatibility.py +0 -85
  144. edsl/jobs/data_structures.py +0 -120
  145. edsl/jobs/decorators.py +0 -35
  146. edsl/jobs/jobs_status_enums.py +0 -9
  147. edsl/jobs/loggers/HTMLTableJobLogger.py +0 -304
  148. edsl/jobs/results_exceptions_handler.py +0 -98
  149. edsl/language_models/ComputeCost.py +0 -63
  150. edsl/language_models/PriceManager.py +0 -127
  151. edsl/language_models/RawResponseHandler.py +0 -106
  152. edsl/language_models/ServiceDataSources.py +0 -0
  153. edsl/language_models/key_management/KeyLookup.py +0 -63
  154. edsl/language_models/key_management/KeyLookupBuilder.py +0 -273
  155. edsl/language_models/key_management/KeyLookupCollection.py +0 -38
  156. edsl/language_models/key_management/__init__.py +0 -0
  157. edsl/language_models/key_management/models.py +0 -131
  158. edsl/language_models/model.py +0 -256
  159. edsl/notebooks/NotebookToLaTeX.py +0 -142
  160. edsl/questions/ExceptionExplainer.py +0 -77
  161. edsl/questions/HTMLQuestion.py +0 -103
  162. edsl/questions/QuestionMatrix.py +0 -265
  163. edsl/questions/data_structures.py +0 -20
  164. edsl/questions/loop_processor.py +0 -149
  165. edsl/questions/response_validator_factory.py +0 -34
  166. edsl/questions/templates/matrix/__init__.py +0 -1
  167. edsl/questions/templates/matrix/answering_instructions.jinja +0 -5
  168. edsl/questions/templates/matrix/question_presentation.jinja +0 -20
  169. edsl/results/MarkdownToDocx.py +0 -122
  170. edsl/results/MarkdownToPDF.py +0 -111
  171. edsl/results/TextEditor.py +0 -50
  172. edsl/results/file_exports.py +0 -252
  173. edsl/results/smart_objects.py +0 -96
  174. edsl/results/table_data_class.py +0 -12
  175. edsl/results/table_renderers.py +0 -118
  176. edsl/scenarios/ConstructDownloadLink.py +0 -109
  177. edsl/scenarios/DocumentChunker.py +0 -102
  178. edsl/scenarios/DocxScenario.py +0 -16
  179. edsl/scenarios/PdfExtractor.py +0 -40
  180. edsl/scenarios/directory_scanner.py +0 -96
  181. edsl/scenarios/file_methods.py +0 -85
  182. edsl/scenarios/handlers/__init__.py +0 -13
  183. edsl/scenarios/handlers/csv.py +0 -49
  184. edsl/scenarios/handlers/docx.py +0 -76
  185. edsl/scenarios/handlers/html.py +0 -37
  186. edsl/scenarios/handlers/json.py +0 -111
  187. edsl/scenarios/handlers/latex.py +0 -5
  188. edsl/scenarios/handlers/md.py +0 -51
  189. edsl/scenarios/handlers/pdf.py +0 -68
  190. edsl/scenarios/handlers/png.py +0 -39
  191. edsl/scenarios/handlers/pptx.py +0 -105
  192. edsl/scenarios/handlers/py.py +0 -294
  193. edsl/scenarios/handlers/sql.py +0 -313
  194. edsl/scenarios/handlers/sqlite.py +0 -149
  195. edsl/scenarios/handlers/txt.py +0 -33
  196. edsl/scenarios/scenario_selector.py +0 -156
  197. edsl/surveys/ConstructDAG.py +0 -92
  198. edsl/surveys/EditSurvey.py +0 -221
  199. edsl/surveys/InstructionHandler.py +0 -100
  200. edsl/surveys/MemoryManagement.py +0 -72
  201. edsl/surveys/RuleManager.py +0 -172
  202. edsl/surveys/Simulator.py +0 -75
  203. edsl/surveys/SurveyToApp.py +0 -141
  204. edsl/utilities/PrettyList.py +0 -56
  205. edsl/utilities/is_notebook.py +0 -18
  206. edsl/utilities/is_valid_variable_name.py +0 -11
  207. edsl/utilities/remove_edsl_version.py +0 -24
  208. edsl-0.1.39.dist-info/RECORD +0 -358
  209. /edsl/questions/{register_questions_meta.py → RegisterQuestionsMeta.py} +0 -0
  210. /edsl/results/{results_fetch_mixin.py → ResultsFetchMixin.py} +0 -0
  211. /edsl/results/{results_tools_mixin.py → ResultsToolsMixin.py} +0 -0
  212. {edsl-0.1.39.dist-info → edsl-0.1.39.dev1.dist-info}/LICENSE +0 -0
edsl/jobs/Jobs.py CHANGED
@@ -1,106 +1,36 @@
1
1
  # """The Jobs class is a collection of agents, scenarios and models and one survey."""
2
2
  from __future__ import annotations
3
- import asyncio
4
- from inspect import signature
5
- from typing import (
6
- Literal,
7
- Optional,
8
- Union,
9
- Sequence,
10
- Generator,
11
- TYPE_CHECKING,
12
- Callable,
13
- Tuple,
14
- )
3
+ import warnings
4
+ import requests
5
+ from itertools import product
6
+ from typing import Literal, Optional, Union, Sequence, Generator, TYPE_CHECKING
15
7
 
16
8
  from edsl.Base import Base
17
9
 
10
+ from edsl.exceptions import MissingAPIKeyError
18
11
  from edsl.jobs.buckets.BucketCollection import BucketCollection
19
- from edsl.jobs.JobsPrompts import JobsPrompts
20
12
  from edsl.jobs.interviews.Interview import Interview
21
- from edsl.utilities.remove_edsl_version import remove_edsl_version
22
13
  from edsl.jobs.runners.JobsRunnerAsyncio import JobsRunnerAsyncio
14
+ from edsl.utilities.decorators import remove_edsl_version
15
+
23
16
  from edsl.data.RemoteCacheSync import RemoteCacheSync
24
17
  from edsl.exceptions.coop import CoopServerResponseError
25
18
 
26
- from edsl.jobs.JobsChecks import JobsChecks
27
- from edsl.jobs.data_structures import RunEnvironment, RunParameters, RunConfig
28
-
29
19
  if TYPE_CHECKING:
30
20
  from edsl.agents.Agent import Agent
31
21
  from edsl.agents.AgentList import AgentList
32
22
  from edsl.language_models.LanguageModel import LanguageModel
33
23
  from edsl.scenarios.Scenario import Scenario
34
- from edsl.scenarios.ScenarioList import ScenarioList
35
24
  from edsl.surveys.Survey import Survey
36
25
  from edsl.results.Results import Results
37
26
  from edsl.results.Dataset import Dataset
38
- from edsl.language_models.ModelList import ModelList
39
- from edsl.data.Cache import Cache
40
- from edsl.language_models.key_management.KeyLookup import KeyLookup
41
-
42
- VisibilityType = Literal["private", "public", "unlisted"]
43
-
44
- from dataclasses import dataclass
45
- from typing import Optional, Union, TypeVar, Callable, cast
46
- from functools import wraps
47
-
48
- try:
49
- from typing import ParamSpec
50
- except ImportError:
51
- from typing_extensions import ParamSpec
52
-
53
-
54
- P = ParamSpec("P")
55
- T = TypeVar("T")
56
-
57
-
58
- from edsl.jobs.check_survey_scenario_compatibility import (
59
- CheckSurveyScenarioCompatibility,
60
- )
61
-
62
-
63
- def with_config(f: Callable[P, T]) -> Callable[P, T]:
64
- "This decorator make it so that the run function parameters match the RunConfig dataclass."
65
- parameter_fields = {
66
- name: field.default
67
- for name, field in RunParameters.__dataclass_fields__.items()
68
- }
69
- environment_fields = {
70
- name: field.default
71
- for name, field in RunEnvironment.__dataclass_fields__.items()
72
- }
73
- combined = {**parameter_fields, **environment_fields}
74
-
75
- @wraps(f)
76
- def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
77
- environment = RunEnvironment(
78
- **{k: v for k, v in kwargs.items() if k in environment_fields}
79
- )
80
- parameters = RunParameters(
81
- **{k: v for k, v in kwargs.items() if k in parameter_fields}
82
- )
83
- config = RunConfig(environment=environment, parameters=parameters)
84
- return f(*args, config=config)
85
-
86
- # Update the wrapper's signature to include all RunConfig parameters
87
- # old_sig = signature(f)
88
- # wrapper.__signature__ = old_sig.replace(
89
- # parameters=list(old_sig.parameters.values())[:-1]
90
- # + [
91
- # old_sig.parameters["config"].replace(
92
- # default=parameter_fields[name], name=name
93
- # )
94
- # for name in combined
95
- # ]
96
- # )
97
-
98
- return cast(Callable[P, T], wrapper)
99
27
 
100
28
 
101
29
  class Jobs(Base):
102
30
  """
103
- A collection of agents, scenarios and models and one survey that creates 'interviews'
31
+ A collection of agents, scenarios and models and one survey.
32
+ The actual running of a job is done by a `JobsRunner`, which is a subclass of `JobsRunner`.
33
+ The `JobsRunner` is chosen by the user, and is stored in the `jobs_runner_name` attribute.
104
34
  """
105
35
 
106
36
  __documentation__ = "https://docs.expectedparrot.com/en/latest/jobs.html"
@@ -108,9 +38,9 @@ class Jobs(Base):
108
38
  def __init__(
109
39
  self,
110
40
  survey: "Survey",
111
- agents: Optional[Union[list[Agent], AgentList]] = None,
112
- models: Optional[Union[ModelList, list[LanguageModel]]] = None,
113
- scenarios: Optional[Union[ScenarioList, list[Scenario]]] = None,
41
+ agents: Optional[list["Agent"]] = None,
42
+ models: Optional[list["LanguageModel"]] = None,
43
+ scenarios: Optional[list["Scenario"]] = None,
114
44
  ):
115
45
  """Initialize a Jobs instance.
116
46
 
@@ -119,62 +49,14 @@ class Jobs(Base):
119
49
  :param models: a list of models
120
50
  :param scenarios: a list of scenarios
121
51
  """
122
- self.run_config = RunConfig(
123
- environment=RunEnvironment(), parameters=RunParameters()
124
- )
125
-
126
52
  self.survey = survey
127
- self.agents: AgentList = agents
128
- self.scenarios: ScenarioList = scenarios
129
- self.models: ModelList = models
130
-
131
- def add_running_env(self, running_env: RunEnvironment):
132
- self.run_config.add_environment(running_env)
133
- return self
134
-
135
- def using_cache(self, cache: "Cache") -> Jobs:
136
- """
137
- Add a Cache to the job.
138
-
139
- :param cache: the cache to add
140
- """
141
- self.run_config.add_cache(cache)
142
- return self
143
-
144
- def using_bucket_collection(self, bucket_collection: BucketCollection) -> Jobs:
145
- """
146
- Add a BucketCollection to the job.
147
-
148
- :param bucket_collection: the bucket collection to add
149
- """
150
- self.run_config.add_bucket_collection(bucket_collection)
151
- return self
53
+ self.agents: "AgentList" = agents
54
+ self.scenarios: "ScenarioList" = scenarios
55
+ self.models = models
152
56
 
153
- def using_key_lookup(self, key_lookup: KeyLookup) -> Jobs:
154
- """
155
- Add a KeyLookup to the job.
156
-
157
- :param key_lookup: the key lookup to add
158
- """
159
- self.run_config.add_key_lookup(key_lookup)
160
- return self
57
+ self.__bucket_collection = None
161
58
 
162
- def using(self, obj: Union[Cache, BucketCollection, KeyLookup]) -> Jobs:
163
- """
164
- Add a Cache, BucketCollection, or KeyLookup to the job.
165
-
166
- :param obj: the object to add
167
- """
168
- from edsl.data.Cache import Cache
169
- from edsl.language_models.key_management.KeyLookup import KeyLookup
170
-
171
- if isinstance(obj, Cache):
172
- self.using_cache(obj)
173
- elif isinstance(obj, BucketCollection):
174
- self.using_bucket_collection(obj)
175
- elif isinstance(obj, KeyLookup):
176
- self.using_key_lookup(obj)
177
- return self
59
+ # these setters and getters are used to ensure that the agents, models, and scenarios are stored as AgentList, ModelList, and ScenarioList objects
178
60
 
179
61
  @property
180
62
  def models(self):
@@ -182,7 +64,7 @@ class Jobs(Base):
182
64
 
183
65
  @models.setter
184
66
  def models(self, value):
185
- from edsl.language_models.ModelList import ModelList
67
+ from edsl import ModelList
186
68
 
187
69
  if value:
188
70
  if not isinstance(value, ModelList):
@@ -192,19 +74,13 @@ class Jobs(Base):
192
74
  else:
193
75
  self._models = ModelList([])
194
76
 
195
- # update the bucket collection if it exists
196
- if self.run_config.environment.bucket_collection is None:
197
- self.run_config.environment.bucket_collection = (
198
- self.create_bucket_collection()
199
- )
200
-
201
77
  @property
202
78
  def agents(self):
203
79
  return self._agents
204
80
 
205
81
  @agents.setter
206
82
  def agents(self, value):
207
- from edsl.agents.AgentList import AgentList
83
+ from edsl import AgentList
208
84
 
209
85
  if value:
210
86
  if not isinstance(value, AgentList):
@@ -220,7 +96,7 @@ class Jobs(Base):
220
96
 
221
97
  @scenarios.setter
222
98
  def scenarios(self, value):
223
- from edsl.scenarios.ScenarioList import ScenarioList
99
+ from edsl import ScenarioList
224
100
  from edsl.results.Dataset import Dataset
225
101
 
226
102
  if value:
@@ -239,32 +115,28 @@ class Jobs(Base):
239
115
  def by(
240
116
  self,
241
117
  *args: Union[
242
- Agent,
243
- Scenario,
244
- LanguageModel,
118
+ "Agent",
119
+ "Scenario",
120
+ "LanguageModel",
245
121
  Sequence[Union["Agent", "Scenario", "LanguageModel"]],
246
122
  ],
247
123
  ) -> Jobs:
248
124
  """
249
- Add Agents, Scenarios and LanguageModels to a job.
250
-
251
- :param args: objects or a sequence (list, tuple, ...) of objects of the same type
252
-
253
- If no objects of this type exist in the Jobs instance, it stores the new objects as a list in the corresponding attribute.
254
- Otherwise, it combines the new objects with existing objects using the object's `__add__` method.
125
+ Add Agents, Scenarios and LanguageModels to a job. If no objects of this type exist in the Jobs instance, it stores the new objects as a list in the corresponding attribute. Otherwise, it combines the new objects with existing objects using the object's `__add__` method.
255
126
 
256
127
  This 'by' is intended to create a fluent interface.
257
128
 
258
- >>> from edsl.surveys.Survey import Survey
259
- >>> from edsl.questions.QuestionFreeText import QuestionFreeText
129
+ >>> from edsl import Survey
130
+ >>> from edsl import QuestionFreeText
260
131
  >>> q = QuestionFreeText(question_name="name", question_text="What is your name?")
261
132
  >>> j = Jobs(survey = Survey(questions=[q]))
262
133
  >>> j
263
134
  Jobs(survey=Survey(...), agents=AgentList([]), models=ModelList([]), scenarios=ScenarioList([]))
264
- >>> from edsl.agents.Agent import Agent; a = Agent(traits = {"status": "Sad"})
135
+ >>> from edsl import Agent; a = Agent(traits = {"status": "Sad"})
265
136
  >>> j.by(a).agents
266
137
  AgentList([Agent(traits = {'status': 'Sad'})])
267
138
 
139
+ :param args: objects or a sequence (list, tuple, ...) of objects of the same type
268
140
 
269
141
  Notes:
270
142
  - all objects must implement the 'get_value', 'set_value', and `__add__` methods
@@ -272,9 +144,28 @@ class Jobs(Base):
272
144
  - scenarios: traits of new scenarios are combined with traits of old existing. New scenarios will overwrite overlapping traits, and do not increase the number of scenarios in the instance
273
145
  - models: new models overwrite old models.
274
146
  """
275
- from edsl.jobs.JobsComponentConstructor import JobsComponentConstructor
147
+ from edsl.results.Dataset import Dataset
148
+
149
+ if isinstance(
150
+ args[0], Dataset
151
+ ): # let the user user a Dataset as if it were a ScenarioList
152
+ args = args[0].to_scenario_list()
153
+
154
+ passed_objects = self._turn_args_to_list(
155
+ args
156
+ ) # objects can also be passed comma-separated
276
157
 
277
- return JobsComponentConstructor(self).by(*args)
158
+ current_objects, objects_key = self._get_current_objects_of_this_type(
159
+ passed_objects[0]
160
+ )
161
+
162
+ if not current_objects:
163
+ new_objects = passed_objects
164
+ else:
165
+ new_objects = self._merge_objects(passed_objects, current_objects)
166
+
167
+ setattr(self, objects_key, new_objects) # update the job
168
+ return self
278
169
 
279
170
  def prompts(self) -> "Dataset":
280
171
  """Return a Dataset of prompts that will be used.
@@ -284,9 +175,12 @@ class Jobs(Base):
284
175
  >>> Jobs.example().prompts()
285
176
  Dataset(...)
286
177
  """
287
- return JobsPrompts(self).prompts()
178
+ from edsl.jobs.JobsPrompts import JobsPrompts
179
+
180
+ j = JobsPrompts(self)
181
+ return j.prompts()
288
182
 
289
- def show_prompts(self, all: bool = False) -> None:
183
+ def show_prompts(self, all=False) -> None:
290
184
  """Print the prompts."""
291
185
  if all:
292
186
  return self.prompts().to_scenario_list().table()
@@ -306,12 +200,9 @@ class Jobs(Base):
306
200
  """
307
201
  Estimate the cost of running the prompts.
308
202
  :param iterations: the number of iterations to run
309
- :param system_prompt: the system prompt
310
- :param user_prompt: the user prompt
311
- :param price_lookup: the price lookup
312
- :param inference_service: the inference service
313
- :param model: the model name
314
203
  """
204
+ from edsl.jobs.JobsPrompts import JobsPrompts
205
+
315
206
  return JobsPrompts.estimate_prompt_cost(
316
207
  system_prompt, user_prompt, price_lookup, inference_service, model
317
208
  )
@@ -322,14 +213,18 @@ class Jobs(Base):
322
213
 
323
214
  :param iterations: the number of iterations to run
324
215
  """
325
- return JobsPrompts(self).estimate_job_cost(iterations)
216
+ from edsl.jobs.JobsPrompts import JobsPrompts
217
+
218
+ j = JobsPrompts(self)
219
+ return j.estimate_job_cost(iterations)
326
220
 
327
221
  def estimate_job_cost_from_external_prices(
328
222
  self, price_lookup: dict, iterations: int = 1
329
223
  ) -> dict:
330
- return JobsPrompts(self).estimate_job_cost_from_external_prices(
331
- price_lookup, iterations
332
- )
224
+ from edsl.jobs.JobsPrompts import JobsPrompts
225
+
226
+ j = JobsPrompts(self)
227
+ return j.estimate_job_cost_from_external_prices(price_lookup, iterations)
333
228
 
334
229
  @staticmethod
335
230
  def compute_job_cost(job_results: Results) -> float:
@@ -338,30 +233,111 @@ class Jobs(Base):
338
233
  """
339
234
  return job_results.compute_job_cost()
340
235
 
341
- def replace_missing_objects(self) -> None:
236
+ @staticmethod
237
+ def _get_container_class(object):
238
+ from edsl.agents.AgentList import AgentList
239
+ from edsl.agents.Agent import Agent
240
+ from edsl.scenarios.Scenario import Scenario
241
+ from edsl.scenarios.ScenarioList import ScenarioList
242
+ from edsl.language_models.ModelList import ModelList
243
+
244
+ if isinstance(object, Agent):
245
+ return AgentList
246
+ elif isinstance(object, Scenario):
247
+ return ScenarioList
248
+ elif isinstance(object, ModelList):
249
+ return ModelList
250
+ else:
251
+ return list
252
+
253
+ @staticmethod
254
+ def _turn_args_to_list(args):
255
+ """Return a list of the first argument if it is a sequence, otherwise returns a list of all the arguments.
256
+
257
+ Example:
258
+
259
+ >>> Jobs._turn_args_to_list([1,2,3])
260
+ [1, 2, 3]
261
+
262
+ """
263
+
264
+ def did_user_pass_a_sequence(args):
265
+ """Return True if the user passed a sequence, False otherwise.
266
+
267
+ Example:
268
+
269
+ >>> did_user_pass_a_sequence([1,2,3])
270
+ True
271
+
272
+ >>> did_user_pass_a_sequence(1)
273
+ False
274
+ """
275
+ return len(args) == 1 and isinstance(args[0], Sequence)
276
+
277
+ if did_user_pass_a_sequence(args):
278
+ container_class = Jobs._get_container_class(args[0][0])
279
+ return container_class(args[0])
280
+ else:
281
+ container_class = Jobs._get_container_class(args[0])
282
+ return container_class(args)
283
+
284
+ def _get_current_objects_of_this_type(
285
+ self, object: Union["Agent", "Scenario", "LanguageModel"]
286
+ ) -> tuple[list, str]:
342
287
  from edsl.agents.Agent import Agent
343
- from edsl.language_models.model import Model
344
288
  from edsl.scenarios.Scenario import Scenario
289
+ from edsl.language_models.LanguageModel import LanguageModel
345
290
 
346
- self.agents = self.agents or [Agent()]
347
- self.models = self.models or [Model()]
348
- self.scenarios = self.scenarios or [Scenario()]
291
+ """Return the current objects of the same type as the first argument.
349
292
 
350
- def generate_interviews(self) -> Generator[Interview, None, None]:
293
+ >>> from edsl.jobs import Jobs
294
+ >>> j = Jobs.example()
295
+ >>> j._get_current_objects_of_this_type(j.agents[0])
296
+ (AgentList([Agent(traits = {'status': 'Joyful'}), Agent(traits = {'status': 'Sad'})]), 'agents')
351
297
  """
352
- Generate interviews.
298
+ class_to_key = {
299
+ Agent: "agents",
300
+ Scenario: "scenarios",
301
+ LanguageModel: "models",
302
+ }
303
+ for class_type in class_to_key:
304
+ if isinstance(object, class_type) or issubclass(
305
+ object.__class__, class_type
306
+ ):
307
+ key = class_to_key[class_type]
308
+ break
309
+ else:
310
+ raise ValueError(
311
+ f"First argument must be an Agent, Scenario, or LanguageModel, not {object}"
312
+ )
313
+ current_objects = getattr(self, key, None)
314
+ return current_objects, key
353
315
 
354
- Note that this sets the agents, model and scenarios if they have not been set. This is a side effect of the method.
355
- This is useful because a user can create a job without setting the agents, models, or scenarios, and the job will still run,
356
- with us filling in defaults.
316
+ @staticmethod
317
+ def _get_empty_container_object(object):
318
+ from edsl.agents.AgentList import AgentList
319
+ from edsl.scenarios.ScenarioList import ScenarioList
320
+
321
+ return {"Agent": AgentList([]), "Scenario": ScenarioList([])}.get(
322
+ object.__class__.__name__, []
323
+ )
357
324
 
325
+ @staticmethod
326
+ def _merge_objects(passed_objects, current_objects) -> list:
358
327
  """
359
- from edsl.jobs.InterviewsConstructor import InterviewsConstructor
328
+ Combine all the existing objects with the new objects.
360
329
 
361
- self.replace_missing_objects()
362
- yield from InterviewsConstructor(
363
- self, cache=self.run_config.environment.cache
364
- ).create_interviews()
330
+ For example, if the user passes in 3 agents,
331
+ and there are 2 existing agents, this will create 6 new agents
332
+
333
+ >>> Jobs(survey = [])._merge_objects([1,2,3], [4,5,6])
334
+ [5, 6, 7, 6, 7, 8, 7, 8, 9]
335
+ """
336
+ new_objects = Jobs._get_empty_container_object(passed_objects[0])
337
+ for current_object in current_objects:
338
+ for new_object in passed_objects:
339
+ new_objects.append(current_object + new_object)
340
+ return new_objects
365
341
 
366
342
  def interviews(self) -> list[Interview]:
367
343
  """
@@ -377,10 +353,13 @@ class Jobs(Base):
377
353
  >>> j.interviews()[0]
378
354
  Interview(agent = Agent(traits = {'status': 'Joyful'}), survey = Survey(...), scenario = Scenario({'period': 'morning'}), model = Model(...))
379
355
  """
380
- return list(self.generate_interviews())
356
+ if hasattr(self, "_interviews"):
357
+ return self._interviews
358
+ else:
359
+ return list(self._create_interviews())
381
360
 
382
361
  @classmethod
383
- def from_interviews(cls, interview_list) -> "Jobs":
362
+ def from_interviews(cls, interview_list):
384
363
  """Return a Jobs instance from a list of interviews.
385
364
 
386
365
  This is useful when you have, say, a list of failed interviews and you want to create
@@ -394,6 +373,34 @@ class Jobs(Base):
394
373
  jobs._interviews = interview_list
395
374
  return jobs
396
375
 
376
+ def _create_interviews(self) -> Generator[Interview, None, None]:
377
+ """
378
+ Generate interviews.
379
+
380
+ Note that this sets the agents, model and scenarios if they have not been set. This is a side effect of the method.
381
+ This is useful because a user can create a job without setting the agents, models, or scenarios, and the job will still run,
382
+ with us filling in defaults.
383
+
384
+
385
+ """
386
+ # if no agents, models, or scenarios are set, set them to defaults
387
+ from edsl.agents.Agent import Agent
388
+ from edsl.language_models.registry import Model
389
+ from edsl.scenarios.Scenario import Scenario
390
+
391
+ self.agents = self.agents or [Agent()]
392
+ self.models = self.models or [Model()]
393
+ self.scenarios = self.scenarios or [Scenario()]
394
+ for agent, scenario, model in product(self.agents, self.scenarios, self.models):
395
+ yield Interview(
396
+ survey=self.survey,
397
+ agent=agent,
398
+ scenario=scenario,
399
+ model=model,
400
+ skip_retry=self.skip_retry,
401
+ raise_validation_errors=self.raise_validation_errors,
402
+ )
403
+
397
404
  def create_bucket_collection(self) -> BucketCollection:
398
405
  """
399
406
  Create a collection of buckets for each model.
@@ -407,7 +414,17 @@ class Jobs(Base):
407
414
  >>> bc
408
415
  BucketCollection(...)
409
416
  """
410
- return BucketCollection.from_models(self.models)
417
+ bucket_collection = BucketCollection()
418
+ for model in self.models:
419
+ bucket_collection.add_model(model)
420
+ return bucket_collection
421
+
422
+ @property
423
+ def bucket_collection(self) -> BucketCollection:
424
+ """Return the bucket collection. If it does not exist, create it."""
425
+ if self.__bucket_collection is None:
426
+ self.__bucket_collection = self.create_bucket_collection()
427
+ return self.__bucket_collection
411
428
 
412
429
  def html(self):
413
430
  """Return the HTML representations for each scenario"""
@@ -434,27 +451,89 @@ class Jobs(Base):
434
451
 
435
452
  def _output(self, message) -> None:
436
453
  """Check if a Job is verbose. If so, print the message."""
437
- if self.run_config.parameters.verbose:
454
+ if hasattr(self, "verbose") and self.verbose:
438
455
  print(message)
439
- # if hasattr(self, "verbose") and self.verbose:
440
- # print(message)
441
456
 
442
- def all_question_parameters(self) -> set:
443
- """Return all the fields in the questions in the survey.
444
- >>> from edsl.jobs import Jobs
445
- >>> Jobs.example().all_question_parameters()
446
- {'period'}
447
- """
448
- return set.union(*[question.parameters for question in self.survey.questions])
457
+ def _check_parameters(self, strict=False, warn=False) -> None:
458
+ """Check if the parameters in the survey and scenarios are consistent.
459
+
460
+ >>> from edsl import QuestionFreeText
461
+ >>> from edsl import Survey
462
+ >>> from edsl import Scenario
463
+ >>> q = QuestionFreeText(question_text = "{{poo}}", question_name = "ugly_question")
464
+ >>> j = Jobs(survey = Survey(questions=[q]))
465
+ >>> with warnings.catch_warnings(record=True) as w:
466
+ ... j._check_parameters(warn = True)
467
+ ... assert len(w) == 1
468
+ ... assert issubclass(w[-1].category, UserWarning)
469
+ ... assert "The following parameters are in the survey but not in the scenarios" in str(w[-1].message)
470
+
471
+ >>> q = QuestionFreeText(question_text = "{{poo}}", question_name = "ugly_question")
472
+ >>> s = Scenario({'plop': "A", 'poo': "B"})
473
+ >>> j = Jobs(survey = Survey(questions=[q])).by(s)
474
+ >>> j._check_parameters(strict = True)
475
+ Traceback (most recent call last):
476
+ ...
477
+ ValueError: The following parameters are in the scenarios but not in the survey: {'plop'}
478
+
479
+ >>> q = QuestionFreeText(question_text = "Hello", question_name = "ugly_question")
480
+ >>> s = Scenario({'ugly_question': "B"})
481
+ >>> j = Jobs(survey = Survey(questions=[q])).by(s)
482
+ >>> j._check_parameters()
483
+ Traceback (most recent call last):
484
+ ...
485
+ ValueError: The following names are in both the survey question_names and the scenario keys: {'ugly_question'}. This will create issues.
486
+ """
487
+ survey_parameters: set = self.survey.parameters
488
+ scenario_parameters: set = self.scenarios.parameters
489
+
490
+ msg0, msg1, msg2 = None, None, None
491
+
492
+ # look for key issues
493
+ if intersection := set(self.scenarios.parameters) & set(
494
+ self.survey.question_names
495
+ ):
496
+ msg0 = f"The following names are in both the survey question_names and the scenario keys: {intersection}. This will create issues."
449
497
 
450
- def use_remote_cache(self) -> bool:
451
- import requests
498
+ raise ValueError(msg0)
452
499
 
453
- if self.run_config.parameters.disable_remote_cache:
500
+ if in_survey_but_not_in_scenarios := survey_parameters - scenario_parameters:
501
+ msg1 = f"The following parameters are in the survey but not in the scenarios: {in_survey_but_not_in_scenarios}"
502
+ if in_scenarios_but_not_in_survey := scenario_parameters - survey_parameters:
503
+ msg2 = f"The following parameters are in the scenarios but not in the survey: {in_scenarios_but_not_in_survey}"
504
+
505
+ if msg1 or msg2:
506
+ message = "\n".join(filter(None, [msg1, msg2]))
507
+ if strict:
508
+ raise ValueError(message)
509
+ else:
510
+ if warn:
511
+ warnings.warn(message)
512
+
513
+ if self.scenarios.has_jinja_braces:
514
+ warnings.warn(
515
+ "The scenarios have Jinja braces ({{ and }}). Converting to '<<' and '>>'. If you want a different conversion, use the convert_jinja_braces method first to modify the scenario."
516
+ )
517
+ self.scenarios = self.scenarios.convert_jinja_braces()
518
+
519
+ @property
520
+ def skip_retry(self):
521
+ if not hasattr(self, "_skip_retry"):
454
522
  return False
455
- if not self.run_config.parameters.disable_remote_cache:
523
+ return self._skip_retry
524
+
525
+ @property
526
+ def raise_validation_errors(self):
527
+ if not hasattr(self, "_raise_validation_errors"):
528
+ return False
529
+ return self._raise_validation_errors
530
+
531
+ def use_remote_cache(self, disable_remote_cache: bool) -> bool:
532
+ if disable_remote_cache:
533
+ return False
534
+ if not disable_remote_cache:
456
535
  try:
457
- from edsl.coop.coop import Coop
536
+ from edsl import Coop
458
537
 
459
538
  user_edsl_settings = Coop().edsl_settings
460
539
  return user_edsl_settings.get("remote_caching", False)
@@ -465,173 +544,152 @@ class Jobs(Base):
465
544
 
466
545
  return False
467
546
 
468
- def _remote_results(
547
+ def run(
469
548
  self,
470
- ) -> Union["Results", None]:
471
- from edsl.jobs.JobsRemoteInferenceHandler import JobsRemoteInferenceHandler
472
-
473
- jh = JobsRemoteInferenceHandler(
474
- self, verbose=self.run_config.parameters.verbose
475
- )
476
- if jh.use_remote_inference(self.run_config.parameters.disable_remote_inference):
477
- job_info = jh.create_remote_inference_job(
478
- iterations=self.run_config.parameters.n,
479
- remote_inference_description=self.run_config.parameters.remote_inference_description,
480
- remote_inference_results_visibility=self.run_config.parameters.remote_inference_results_visibility,
481
- )
482
- results = jh.poll_remote_inference_job(job_info)
483
- return results
484
- else:
485
- return None
486
-
487
- def _prepare_to_run(self) -> None:
488
- "This makes sure that the job is ready to run and that keys are in place for a remote job."
489
- CheckSurveyScenarioCompatibility(self.survey, self.scenarios).check()
490
-
491
- def _check_if_remote_keys_ok(self):
492
- jc = JobsChecks(self)
493
- if jc.needs_key_process():
494
- jc.key_process()
495
-
496
- def _check_if_local_keys_ok(self):
497
- jc = JobsChecks(self)
498
- if self.run_config.parameters.check_api_keys:
499
- jc.check_api_keys()
500
-
501
- async def _execute_with_remote_cache(self, run_job_async: bool) -> Results:
502
-
503
- use_remote_cache = self.use_remote_cache()
549
+ n: int = 1,
550
+ progress_bar: bool = False,
551
+ stop_on_exception: bool = False,
552
+ cache: Union[Cache, bool] = None,
553
+ check_api_keys: bool = False,
554
+ sidecar_model: Optional[LanguageModel] = None,
555
+ verbose: bool = True,
556
+ print_exceptions=True,
557
+ remote_cache_description: Optional[str] = None,
558
+ remote_inference_description: Optional[str] = None,
559
+ remote_inference_results_visibility: Optional[
560
+ Literal["private", "public", "unlisted"]
561
+ ] = "unlisted",
562
+ skip_retry: bool = False,
563
+ raise_validation_errors: bool = False,
564
+ disable_remote_cache: bool = False,
565
+ disable_remote_inference: bool = False,
566
+ ) -> Results:
567
+ """
568
+ Runs the Job: conducts Interviews and returns their results.
504
569
 
570
+ :param n: How many times to run each interview
571
+ :param progress_bar: Whether to show a progress bar
572
+ :param stop_on_exception: Stops the job if an exception is raised
573
+ :param cache: A Cache object to store results
574
+ :param check_api_keys: Raises an error if API keys are invalid
575
+ :param verbose: Prints extra messages
576
+ :param remote_cache_description: Specifies a description for this group of entries in the remote cache
577
+ :param remote_inference_description: Specifies a description for the remote inference job
578
+ :param remote_inference_results_visibility: The initial visibility of the Results object on Coop. This will only be used for remote jobs!
579
+ :param disable_remote_cache: If True, the job will not use remote cache. This only works for local jobs!
580
+ :param disable_remote_inference: If True, the job will not use remote inference
581
+ """
505
582
  from edsl.coop.coop import Coop
506
- from edsl.jobs.runners.JobsRunnerAsyncio import JobsRunnerAsyncio
507
- from edsl.data.Cache import Cache
508
583
 
509
- assert isinstance(self.run_config.environment.cache, Cache)
584
+ self._check_parameters()
585
+ self._skip_retry = skip_retry
586
+ self._raise_validation_errors = raise_validation_errors
510
587
 
511
- with RemoteCacheSync(
512
- coop=Coop(),
513
- cache=self.run_config.environment.cache,
514
- output_func=self._output,
515
- remote_cache=use_remote_cache,
516
- remote_cache_description=self.run_config.parameters.remote_cache_description,
517
- ):
518
- runner = JobsRunnerAsyncio(self, environment=self.run_config.environment)
519
- if run_job_async:
520
- results = await runner.run_async(self.run_config.parameters)
521
- else:
522
- results = runner.run(self.run_config.parameters)
523
- return results
588
+ self.verbose = verbose
524
589
 
525
- def _setup_and_check(self) -> Tuple[RunConfig, Optional[Results]]:
590
+ from edsl.jobs.JobsChecks import JobsChecks
526
591
 
527
- self._prepare_to_run()
528
- self._check_if_remote_keys_ok()
529
-
530
- # first try to run the job remotely
531
- if results := self._remote_results():
532
- return results
533
-
534
- self._check_if_local_keys_ok()
535
- return None
592
+ jc = JobsChecks(self)
536
593
 
537
- @property
538
- def num_interviews(self):
539
- if self.run_config.parameters.n is None:
540
- return len(self)
541
- else:
542
- len(self) * self.run_config.parameters.n
594
+ # check if the user has all the keys they need
595
+ if jc.needs_key_process():
596
+ jc.key_process()
543
597
 
544
- def _run(self, config: RunConfig):
545
- "Shared code for run and run_async"
546
- if config.environment.cache is not None:
547
- self.run_config.environment.cache = config.environment.cache
598
+ from edsl.jobs.JobsRemoteInferenceHandler import JobsRemoteInferenceHandler
548
599
 
549
- if config.environment.bucket_collection is not None:
550
- self.run_config.environment.bucket_collection = (
551
- config.environment.bucket_collection
600
+ jh = JobsRemoteInferenceHandler(self, verbose=verbose)
601
+ if jh.use_remote_inference(disable_remote_inference):
602
+ jh.create_remote_inference_job(
603
+ iterations=n,
604
+ remote_inference_description=remote_inference_description,
605
+ remote_inference_results_visibility=remote_inference_results_visibility,
552
606
  )
607
+ results = jh.poll_remote_inference_job()
608
+ return results
553
609
 
554
- if config.environment.key_lookup is not None:
555
- self.run_config.environment.key_lookup = config.environment.key_lookup
556
-
557
- # replace the parameters with the ones from the config
558
- self.run_config.parameters = config.parameters
559
-
560
- self.replace_missing_objects()
561
-
562
- # try to run remotely first
563
- self._prepare_to_run()
564
- self._check_if_remote_keys_ok()
610
+ if check_api_keys:
611
+ jc.check_api_keys()
565
612
 
566
- if (
567
- self.run_config.environment.cache is None
568
- or self.run_config.environment.cache is True
569
- ):
613
+ # handle cache
614
+ if cache is None or cache is True:
570
615
  from edsl.data.CacheHandler import CacheHandler
571
616
 
572
- self.run_config.environment.cache = CacheHandler().get_cache()
573
-
574
- if self.run_config.environment.cache is False:
617
+ cache = CacheHandler().get_cache()
618
+ if cache is False:
575
619
  from edsl.data.Cache import Cache
576
620
 
577
- self.run_config.environment.cache = Cache(immediate_write=False)
621
+ cache = Cache()
578
622
 
579
- # first try to run the job remotely
580
- if results := self._remote_results():
581
- return results
623
+ remote_cache = self.use_remote_cache(disable_remote_cache)
624
+ with RemoteCacheSync(
625
+ coop=Coop(),
626
+ cache=cache,
627
+ output_func=self._output,
628
+ remote_cache=remote_cache,
629
+ remote_cache_description=remote_cache_description,
630
+ ) as r:
631
+ results = self._run_local(
632
+ n=n,
633
+ progress_bar=progress_bar,
634
+ cache=cache,
635
+ stop_on_exception=stop_on_exception,
636
+ sidecar_model=sidecar_model,
637
+ print_exceptions=print_exceptions,
638
+ raise_validation_errors=raise_validation_errors,
639
+ )
582
640
 
583
- self._check_if_local_keys_ok()
641
+ # results.cache = cache.new_entries_cache()
642
+ return results
584
643
 
585
- if config.environment.bucket_collection is None:
586
- self.run_config.environment.bucket_collection = (
587
- self.create_bucket_collection()
588
- )
644
+ async def run_async(
645
+ self,
646
+ cache=None,
647
+ n=1,
648
+ disable_remote_inference: bool = False,
649
+ remote_inference_description: Optional[str] = None,
650
+ remote_inference_results_visibility: Optional[
651
+ Literal["private", "public", "unlisted"]
652
+ ] = "unlisted",
653
+ **kwargs,
654
+ ):
655
+ """Run the job asynchronously, either locally or remotely.
589
656
 
590
- @with_config
591
- def run(self, *, config: RunConfig) -> "Results":
657
+ :param cache: Cache object or boolean
658
+ :param n: Number of iterations
659
+ :param disable_remote_inference: If True, forces local execution
660
+ :param remote_inference_description: Description for remote jobs
661
+ :param remote_inference_results_visibility: Visibility setting for remote results
662
+ :param kwargs: Additional arguments passed to local execution
663
+ :return: Results object
592
664
  """
593
- Runs the Job: conducts Interviews and returns their results.
665
+ # Check if we should use remote inference
666
+ from edsl.jobs.JobsRemoteInferenceHandler import JobsRemoteInferenceHandler
594
667
 
595
- :param n: How many times to run each interview
596
- :param progress_bar: Whether to show a progress bar
597
- :param stop_on_exception: Stops the job if an exception is raised
598
- :param check_api_keys: Raises an error if API keys are invalid
599
- :param verbose: Prints extra messages
600
- :param remote_cache_description: Specifies a description for this group of entries in the remote cache
601
- :param remote_inference_description: Specifies a description for the remote inference job
602
- :param remote_inference_results_visibility: The initial visibility of the Results object on Coop. This will only be used for remote jobs!
603
- :param disable_remote_cache: If True, the job will not use remote cache. This only works for local jobs!
604
- :param disable_remote_inference: If True, the job will not use remote inference
605
- :param cache: A Cache object to store results
606
- :param bucket_collection: A BucketCollection object to track API calls
607
- :param key_lookup: A KeyLookup object to manage API keys
608
- """
609
- self._run(config)
668
+ jh = JobsRemoteInferenceHandler(self, verbose=False)
669
+ if jh.use_remote_inference(disable_remote_inference):
670
+ results = await jh.create_and_poll_remote_job(
671
+ iterations=n,
672
+ remote_inference_description=remote_inference_description,
673
+ remote_inference_results_visibility=remote_inference_results_visibility,
674
+ )
675
+ return results
610
676
 
611
- return asyncio.run(self._execute_with_remote_cache(run_job_async=False))
677
+ # If not using remote inference, run locally with async
678
+ return await JobsRunnerAsyncio(self).run_async(cache=cache, n=n, **kwargs)
612
679
 
613
- @with_config
614
- async def run_async(self, *, config: RunConfig) -> "Results":
615
- """
616
- Runs the Job: conducts Interviews and returns their results.
680
+ def _run_local(self, *args, **kwargs):
681
+ """Run the job locally."""
617
682
 
618
- :param n: How many times to run each interview
619
- :param progress_bar: Whether to show a progress bar
620
- :param stop_on_exception: Stops the job if an exception is raised
621
- :param check_api_keys: Raises an error if API keys are invalid
622
- :param verbose: Prints extra messages
623
- :param remote_cache_description: Specifies a description for this group of entries in the remote cache
624
- :param remote_inference_description: Specifies a description for the remote inference job
625
- :param remote_inference_results_visibility: The initial visibility of the Results object on Coop. This will only be used for remote jobs!
626
- :param disable_remote_cache: If True, the job will not use remote cache. This only works for local jobs!
627
- :param disable_remote_inference: If True, the job will not use remote inference
628
- :param cache: A Cache object to store results
629
- :param bucket_collection: A BucketCollection object to track API calls
630
- :param key_lookup: A KeyLookup object to manage API keys
631
- """
632
- self._run(config)
683
+ results = JobsRunnerAsyncio(self).run(*args, **kwargs)
684
+ return results
633
685
 
634
- return await self._execute_with_remote_cache(run_job_async=True)
686
+ def all_question_parameters(self):
687
+ """Return all the fields in the questions in the survey.
688
+ >>> from edsl.jobs import Jobs
689
+ >>> Jobs.example().all_question_parameters()
690
+ {'period'}
691
+ """
692
+ return set.union(*[question.parameters for question in self.survey.questions])
635
693
 
636
694
  def __repr__(self) -> str:
637
695
  """Return an eval-able string representation of the Jobs instance."""
@@ -639,12 +697,17 @@ class Jobs(Base):
639
697
 
640
698
  def _summary(self):
641
699
  return {
642
- "questions": len(self.survey),
643
- "agents": len(self.agents or [1]),
644
- "models": len(self.models or [1]),
645
- "scenarios": len(self.scenarios or [1]),
700
+ "EDSL Class": "Jobs",
701
+ "Number of questions": len(self.survey),
702
+ "Number of agents": len(self.agents),
703
+ "Number of models": len(self.models),
704
+ "Number of scenarios": len(self.scenarios),
646
705
  }
647
706
 
707
+ def _repr_html_(self) -> str:
708
+ footer = f"<a href={self.__documentation__}>(docs)</a>"
709
+ return str(self.summary(format="html")) + footer
710
+
648
711
  def __len__(self) -> int:
649
712
  """Return the maximum number of questions that will be asked while running this job.
650
713
  Note that this is the maximum number of questions, not the actual number of questions that will be asked, as some questions may be skipped.
@@ -661,6 +724,10 @@ class Jobs(Base):
661
724
  )
662
725
  return number_of_questions
663
726
 
727
+ #######################
728
+ # Serialization methods
729
+ #######################
730
+
664
731
  def to_dict(self, add_edsl_version=True):
665
732
  d = {
666
733
  "survey": self.survey.to_dict(add_edsl_version=add_edsl_version),
@@ -685,14 +752,11 @@ class Jobs(Base):
685
752
 
686
753
  return d
687
754
 
688
- def table(self):
689
- return self.prompts().to_scenario_list().table()
690
-
691
755
  @classmethod
692
756
  @remove_edsl_version
693
757
  def from_dict(cls, data: dict) -> Jobs:
694
758
  """Creates a Jobs instance from a dictionary."""
695
- from edsl.surveys.Survey import Survey
759
+ from edsl import Survey
696
760
  from edsl.agents.Agent import Agent
697
761
  from edsl.language_models.LanguageModel import LanguageModel
698
762
  from edsl.scenarios.Scenario import Scenario
@@ -714,6 +778,9 @@ class Jobs(Base):
714
778
  """
715
779
  return hash(self) == hash(other)
716
780
 
781
+ #######################
782
+ # Example methods
783
+ #######################
717
784
  @classmethod
718
785
  def example(
719
786
  cls,
@@ -733,14 +800,14 @@ class Jobs(Base):
733
800
  """
734
801
  import random
735
802
  from uuid import uuid4
736
- from edsl.questions.QuestionMultipleChoice import QuestionMultipleChoice
803
+ from edsl.questions import QuestionMultipleChoice
737
804
  from edsl.agents.Agent import Agent
738
805
  from edsl.scenarios.Scenario import Scenario
739
806
 
740
807
  addition = "" if not randomize else str(uuid4())
741
808
 
742
809
  if test_model:
743
- from edsl.language_models.LanguageModel import LanguageModel
810
+ from edsl.language_models import LanguageModel
744
811
 
745
812
  m = LanguageModel.example(test_model=True)
746
813
 
@@ -781,8 +848,7 @@ class Jobs(Base):
781
848
  question_options=["Good", "Great", "OK", "Terrible"],
782
849
  question_name="how_feeling_yesterday",
783
850
  )
784
- from edsl.surveys.Survey import Survey
785
- from edsl.scenarios.ScenarioList import ScenarioList
851
+ from edsl import Survey, ScenarioList
786
852
 
787
853
  base_survey = Survey(questions=[q1, q2])
788
854
 
@@ -799,6 +865,15 @@ class Jobs(Base):
799
865
 
800
866
  return job
801
867
 
868
+ def rich_print(self):
869
+ """Print a rich representation of the Jobs instance."""
870
+ from rich.table import Table
871
+
872
+ table = Table(title="Jobs")
873
+ table.add_column("Jobs")
874
+ table.add_row(self.survey.rich_print())
875
+ return table
876
+
802
877
  def code(self):
803
878
  """Return the code to create this instance."""
804
879
  raise NotImplementedError
@@ -806,7 +881,7 @@ class Jobs(Base):
806
881
 
807
882
  def main():
808
883
  """Run the module's doctests."""
809
- from edsl.jobs.Jobs import Jobs
884
+ from edsl.jobs import Jobs
810
885
  from edsl.data.Cache import Cache
811
886
 
812
887
  job = Jobs.example()