edsl 0.1.27.dev2__py3-none-any.whl → 0.1.29__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 (119) hide show
  1. edsl/Base.py +107 -30
  2. edsl/BaseDiff.py +260 -0
  3. edsl/__init__.py +25 -21
  4. edsl/__version__.py +1 -1
  5. edsl/agents/Agent.py +103 -46
  6. edsl/agents/AgentList.py +97 -13
  7. edsl/agents/Invigilator.py +23 -10
  8. edsl/agents/InvigilatorBase.py +19 -14
  9. edsl/agents/PromptConstructionMixin.py +342 -100
  10. edsl/agents/descriptors.py +5 -2
  11. edsl/base/Base.py +289 -0
  12. edsl/config.py +2 -1
  13. edsl/conjure/AgentConstructionMixin.py +152 -0
  14. edsl/conjure/Conjure.py +56 -0
  15. edsl/conjure/InputData.py +659 -0
  16. edsl/conjure/InputDataCSV.py +48 -0
  17. edsl/conjure/InputDataMixinQuestionStats.py +182 -0
  18. edsl/conjure/InputDataPyRead.py +91 -0
  19. edsl/conjure/InputDataSPSS.py +8 -0
  20. edsl/conjure/InputDataStata.py +8 -0
  21. edsl/conjure/QuestionOptionMixin.py +76 -0
  22. edsl/conjure/QuestionTypeMixin.py +23 -0
  23. edsl/conjure/RawQuestion.py +65 -0
  24. edsl/conjure/SurveyResponses.py +7 -0
  25. edsl/conjure/__init__.py +9 -4
  26. edsl/conjure/examples/placeholder.txt +0 -0
  27. edsl/conjure/naming_utilities.py +263 -0
  28. edsl/conjure/utilities.py +165 -28
  29. edsl/conversation/Conversation.py +238 -0
  30. edsl/conversation/car_buying.py +58 -0
  31. edsl/conversation/mug_negotiation.py +81 -0
  32. edsl/conversation/next_speaker_utilities.py +93 -0
  33. edsl/coop/coop.py +337 -121
  34. edsl/coop/utils.py +56 -70
  35. edsl/data/Cache.py +74 -22
  36. edsl/data/CacheHandler.py +10 -9
  37. edsl/data/SQLiteDict.py +11 -3
  38. edsl/inference_services/AnthropicService.py +1 -0
  39. edsl/inference_services/DeepInfraService.py +20 -13
  40. edsl/inference_services/GoogleService.py +7 -1
  41. edsl/inference_services/InferenceServicesCollection.py +33 -7
  42. edsl/inference_services/OpenAIService.py +17 -10
  43. edsl/inference_services/models_available_cache.py +69 -0
  44. edsl/inference_services/rate_limits_cache.py +25 -0
  45. edsl/inference_services/write_available.py +10 -0
  46. edsl/jobs/Answers.py +15 -1
  47. edsl/jobs/Jobs.py +322 -73
  48. edsl/jobs/buckets/BucketCollection.py +9 -3
  49. edsl/jobs/buckets/ModelBuckets.py +4 -2
  50. edsl/jobs/buckets/TokenBucket.py +1 -2
  51. edsl/jobs/interviews/Interview.py +7 -10
  52. edsl/jobs/interviews/InterviewStatusMixin.py +3 -3
  53. edsl/jobs/interviews/InterviewTaskBuildingMixin.py +39 -20
  54. edsl/jobs/interviews/retry_management.py +4 -4
  55. edsl/jobs/runners/JobsRunnerAsyncio.py +103 -65
  56. edsl/jobs/runners/JobsRunnerStatusData.py +3 -3
  57. edsl/jobs/tasks/QuestionTaskCreator.py +4 -2
  58. edsl/jobs/tasks/TaskHistory.py +4 -3
  59. edsl/language_models/LanguageModel.py +42 -55
  60. edsl/language_models/ModelList.py +96 -0
  61. edsl/language_models/registry.py +14 -0
  62. edsl/language_models/repair.py +97 -25
  63. edsl/notebooks/Notebook.py +157 -32
  64. edsl/prompts/Prompt.py +31 -19
  65. edsl/questions/QuestionBase.py +145 -23
  66. edsl/questions/QuestionBudget.py +5 -6
  67. edsl/questions/QuestionCheckBox.py +7 -3
  68. edsl/questions/QuestionExtract.py +5 -3
  69. edsl/questions/QuestionFreeText.py +3 -3
  70. edsl/questions/QuestionFunctional.py +0 -3
  71. edsl/questions/QuestionList.py +3 -4
  72. edsl/questions/QuestionMultipleChoice.py +16 -8
  73. edsl/questions/QuestionNumerical.py +4 -3
  74. edsl/questions/QuestionRank.py +5 -3
  75. edsl/questions/__init__.py +4 -3
  76. edsl/questions/descriptors.py +9 -4
  77. edsl/questions/question_registry.py +27 -31
  78. edsl/questions/settings.py +1 -1
  79. edsl/results/Dataset.py +31 -0
  80. edsl/results/DatasetExportMixin.py +493 -0
  81. edsl/results/Result.py +42 -82
  82. edsl/results/Results.py +178 -66
  83. edsl/results/ResultsDBMixin.py +10 -9
  84. edsl/results/ResultsExportMixin.py +23 -507
  85. edsl/results/ResultsGGMixin.py +3 -3
  86. edsl/results/ResultsToolsMixin.py +9 -9
  87. edsl/scenarios/FileStore.py +140 -0
  88. edsl/scenarios/Scenario.py +59 -6
  89. edsl/scenarios/ScenarioList.py +138 -52
  90. edsl/scenarios/ScenarioListExportMixin.py +32 -0
  91. edsl/scenarios/ScenarioListPdfMixin.py +2 -1
  92. edsl/scenarios/__init__.py +1 -0
  93. edsl/study/ObjectEntry.py +173 -0
  94. edsl/study/ProofOfWork.py +113 -0
  95. edsl/study/SnapShot.py +73 -0
  96. edsl/study/Study.py +498 -0
  97. edsl/study/__init__.py +4 -0
  98. edsl/surveys/MemoryPlan.py +11 -4
  99. edsl/surveys/Survey.py +124 -37
  100. edsl/surveys/SurveyExportMixin.py +25 -5
  101. edsl/surveys/SurveyFlowVisualizationMixin.py +6 -4
  102. edsl/tools/plotting.py +4 -2
  103. edsl/utilities/__init__.py +21 -20
  104. edsl/utilities/gcp_bucket/__init__.py +0 -0
  105. edsl/utilities/gcp_bucket/cloud_storage.py +96 -0
  106. edsl/utilities/gcp_bucket/simple_example.py +9 -0
  107. edsl/utilities/interface.py +90 -73
  108. edsl/utilities/repair_functions.py +28 -0
  109. edsl/utilities/utilities.py +59 -6
  110. {edsl-0.1.27.dev2.dist-info → edsl-0.1.29.dist-info}/METADATA +42 -15
  111. edsl-0.1.29.dist-info/RECORD +203 -0
  112. edsl/conjure/RawResponseColumn.py +0 -327
  113. edsl/conjure/SurveyBuilder.py +0 -308
  114. edsl/conjure/SurveyBuilderCSV.py +0 -78
  115. edsl/conjure/SurveyBuilderSPSS.py +0 -118
  116. edsl/data/RemoteDict.py +0 -103
  117. edsl-0.1.27.dev2.dist-info/RECORD +0 -172
  118. {edsl-0.1.27.dev2.dist-info → edsl-0.1.29.dist-info}/LICENSE +0 -0
  119. {edsl-0.1.27.dev2.dist-info → edsl-0.1.29.dist-info}/WHEEL +0 -0
@@ -6,15 +6,9 @@ import asyncio
6
6
  import time
7
7
  from typing import Any, Type, List, Generator, Optional
8
8
 
9
- from edsl.agents import Agent
10
- from edsl.language_models import LanguageModel
11
- from edsl.scenarios import Scenario
12
- from edsl.surveys import Survey
13
-
14
9
  from edsl.jobs.Answers import Answers
15
10
  from edsl.surveys.base import EndOfSurvey
16
11
  from edsl.jobs.buckets.ModelBuckets import ModelBuckets
17
-
18
12
  from edsl.jobs.tasks.TaskCreators import TaskCreators
19
13
 
20
14
  from edsl.jobs.interviews.InterviewStatusLog import InterviewStatusLog
@@ -60,9 +54,9 @@ class Interview(InterviewStatusMixin, InterviewTaskBuildingMixin):
60
54
  self.debug = debug
61
55
  self.iteration = iteration
62
56
  self.cache = cache
63
- self.answers: dict[
64
- str, str
65
- ] = Answers() # will get filled in as interview progresses
57
+ self.answers: dict[str, str] = (
58
+ Answers()
59
+ ) # will get filled in as interview progresses
66
60
  self.sidecar_model = sidecar_model
67
61
 
68
62
  # Trackers
@@ -101,9 +95,12 @@ class Interview(InterviewStatusMixin, InterviewTaskBuildingMixin):
101
95
  self.sidecar_model = sidecar_model
102
96
 
103
97
  # if no model bucket is passed, create an 'infinity' bucket with no rate limits
104
- if model_buckets is None:
98
+ # print("model_buckets", model_buckets)
99
+ if model_buckets is None or hasattr(self.agent, "answer_question_directly"):
105
100
  model_buckets = ModelBuckets.infinity_bucket()
106
101
 
102
+ # model_buckets = ModelBuckets.infinity_bucket()
103
+
107
104
  ## build the tasks using the InterviewTaskBuildingMixin
108
105
  ## This is the key part---it creates a task for each question,
109
106
  ## with dependencies on the questions that must be answered before this one can be answered.
@@ -17,9 +17,9 @@ class InterviewStatusMixin:
17
17
  The keys are the question names; the values are the lists of status log changes for each task.
18
18
  """
19
19
  for task_creator in self.task_creators.values():
20
- self._task_status_log_dict[
21
- task_creator.question.question_name
22
- ] = task_creator.status_log
20
+ self._task_status_log_dict[task_creator.question.question_name] = (
21
+ task_creator.status_log
22
+ )
23
23
  return self._task_status_log_dict
24
24
 
25
25
  @property
@@ -5,17 +5,19 @@ import asyncio
5
5
  import time
6
6
  import traceback
7
7
  from typing import Generator, Union
8
+
8
9
  from edsl import CONFIG
9
10
  from edsl.exceptions import InterviewTimeoutError
10
- from edsl.data_transfer_models import AgentResponseDict
11
- from edsl.questions.QuestionBase import QuestionBase
11
+
12
+ # from edsl.questions.QuestionBase import QuestionBase
12
13
  from edsl.surveys.base import EndOfSurvey
13
14
  from edsl.jobs.buckets.ModelBuckets import ModelBuckets
14
15
  from edsl.jobs.interviews.interview_exception_tracking import InterviewExceptionEntry
15
16
  from edsl.jobs.interviews.retry_management import retry_strategy
16
17
  from edsl.jobs.tasks.task_status_enum import TaskStatus
17
18
  from edsl.jobs.tasks.QuestionTaskCreator import QuestionTaskCreator
18
- from edsl.agents.InvigilatorBase import InvigilatorBase
19
+
20
+ # from edsl.agents.InvigilatorBase import InvigilatorBase
19
21
 
20
22
  TIMEOUT = float(CONFIG.get("EDSL_API_TIMEOUT"))
21
23
 
@@ -44,6 +46,7 @@ class InterviewTaskBuildingMixin:
44
46
  scenario=self.scenario,
45
47
  model=self.model,
46
48
  debug=debug,
49
+ survey=self.survey,
47
50
  memory_plan=self.survey.memory_plan,
48
51
  current_answers=self.answers,
49
52
  iteration=self.iteration,
@@ -149,29 +152,45 @@ class InterviewTaskBuildingMixin:
149
152
  async def _answer_question_and_record_task(
150
153
  self,
151
154
  *,
152
- question: QuestionBase,
155
+ question: "QuestionBase",
153
156
  debug: bool,
154
157
  task=None,
155
- ) -> AgentResponseDict:
158
+ ) -> "AgentResponseDict":
156
159
  """Answer a question and records the task.
157
160
 
158
161
  This in turn calls the the passed-in agent's async_answer_question method, which returns a response dictionary.
159
162
  Note that is updates answers dictionary with the response.
160
163
  """
161
- invigilator = self._get_invigilator(question, debug=debug)
164
+ from edsl.data_transfer_models import AgentResponseDict
165
+
166
+ try:
167
+ invigilator = self._get_invigilator(question, debug=debug)
162
168
 
163
- if self._skip_this_question(question):
164
- return invigilator.get_failed_task_result()
169
+ if self._skip_this_question(question):
170
+ return invigilator.get_failed_task_result()
165
171
 
166
- response: AgentResponseDict = await self._attempt_to_answer_question(
167
- invigilator, task
168
- )
172
+ response: AgentResponseDict = await self._attempt_to_answer_question(
173
+ invigilator, task
174
+ )
169
175
 
170
- self._add_answer(response=response, question=question)
176
+ self._add_answer(response=response, question=question)
171
177
 
172
- # With the answer to the question, we can now cancel any skipped questions
173
- self._cancel_skipped_questions(question)
174
- return AgentResponseDict(**response)
178
+ # With the answer to the question, we can now cancel any skipped questions
179
+ self._cancel_skipped_questions(question)
180
+ return AgentResponseDict(**response)
181
+ except Exception as e:
182
+ raise e
183
+ # import traceback
184
+ # print("Exception caught:")
185
+ # traceback.print_exc()
186
+
187
+ # # Extract and print the traceback info
188
+ # tb = e.__traceback__
189
+ # while tb is not None:
190
+ # print(f"File {tb.tb_frame.f_code.co_filename}, line {tb.tb_lineno}, in {tb.tb_frame.f_code.co_name}")
191
+ # tb = tb.tb_next
192
+ # breakpoint()
193
+ # raise e
175
194
 
176
195
  def _add_answer(self, response: AgentResponseDict, question: QuestionBase) -> None:
177
196
  """Add the answer to the answers dictionary.
@@ -239,11 +258,11 @@ class InterviewTaskBuildingMixin:
239
258
  """
240
259
  current_question_index: int = self.to_index[current_question.question_name]
241
260
 
242
- next_question: Union[
243
- int, EndOfSurvey
244
- ] = self.survey.rule_collection.next_question(
245
- q_now=current_question_index,
246
- answers=self.answers | self.scenario | self.agent["traits"],
261
+ next_question: Union[int, EndOfSurvey] = (
262
+ self.survey.rule_collection.next_question(
263
+ q_now=current_question_index,
264
+ answers=self.answers | self.scenario | self.agent["traits"],
265
+ )
247
266
  )
248
267
 
249
268
  next_question_index = next_question.next_q
@@ -13,17 +13,17 @@ EDSL_MAX_BACKOFF_SEC = float(CONFIG.get("EDSL_MAX_BACKOFF_SEC"))
13
13
  EDSL_MAX_ATTEMPTS = int(CONFIG.get("EDSL_MAX_ATTEMPTS"))
14
14
 
15
15
 
16
- def print_retry(retry_state, print_to_terminal=False):
16
+ def print_retry(retry_state, print_to_terminal=True):
17
17
  "Prints details on tenacity retries."
18
18
  attempt_number = retry_state.attempt_number
19
19
  exception = retry_state.outcome.exception()
20
20
  wait_time = retry_state.next_action.sleep
21
- # breakpoint()
22
21
  if print_to_terminal:
23
22
  print(
24
- f"Attempt {attempt_number} failed with exception: {repr(exception)}; "
23
+ f"Attempt {attempt_number} failed with exception:" f"{exception}",
25
24
  f"now waiting {wait_time:.2f} seconds before retrying."
26
- f" Parameters: start={EDSL_BACKOFF_START_SEC}, max={EDSL_MAX_BACKOFF_SEC}, max_attempts={EDSL_MAX_ATTEMPTS}."
25
+ f"Parameters: start={EDSL_BACKOFF_START_SEC}, max={EDSL_MAX_BACKOFF_SEC}, max_attempts={EDSL_MAX_ATTEMPTS}."
26
+ "\n\n",
27
27
  )
28
28
 
29
29
 
@@ -1,26 +1,17 @@
1
1
  from __future__ import annotations
2
2
  import time
3
3
  import asyncio
4
- import textwrap
4
+ import time
5
5
  from contextlib import contextmanager
6
6
 
7
7
  from typing import Coroutine, List, AsyncGenerator, Optional, Union
8
8
 
9
- from rich.live import Live
10
- from rich.console import Console
11
-
12
9
  from edsl import shared_globals
13
- from edsl.results import Results, Result
14
-
15
10
  from edsl.jobs.interviews.Interview import Interview
16
- from edsl.utilities.decorators import jupyter_nb_handler
17
- from edsl.jobs.Jobs import Jobs
18
11
  from edsl.jobs.runners.JobsRunnerStatusMixin import JobsRunnerStatusMixin
19
- from edsl.language_models import LanguageModel
20
- from edsl.data.Cache import Cache
21
-
22
12
  from edsl.jobs.tasks.TaskHistory import TaskHistory
23
13
  from edsl.jobs.buckets.BucketCollection import BucketCollection
14
+ from edsl.utilities.decorators import jupyter_nb_handler
24
15
 
25
16
 
26
17
  class JobsRunnerAsyncio(JobsRunnerStatusMixin):
@@ -32,20 +23,20 @@ class JobsRunnerAsyncio(JobsRunnerStatusMixin):
32
23
 
33
24
  def __init__(self, jobs: Jobs):
34
25
  self.jobs = jobs
35
-
26
+ # this creates the interviews, which can take a while
36
27
  self.interviews: List["Interview"] = jobs.interviews()
37
28
  self.bucket_collection: "BucketCollection" = jobs.bucket_collection
38
29
  self.total_interviews: List["Interview"] = []
39
30
 
40
- async def run_async(
31
+ async def run_async_generator(
41
32
  self,
42
- cache: Cache,
33
+ cache: "Cache",
43
34
  n: int = 1,
44
35
  debug: bool = False,
45
36
  stop_on_exception: bool = False,
46
37
  sidecar_model: "LanguageModel" = None,
47
38
  total_interviews: Optional[List["Interview"]] = None,
48
- ) -> AsyncGenerator[Result, None]:
39
+ ) -> AsyncGenerator["Result", None]:
49
40
  """Creates the tasks, runs them asynchronously, and returns the results as a Results object.
50
41
 
51
42
  Completed tasks are yielded as they are completed.
@@ -96,6 +87,20 @@ class JobsRunnerAsyncio(JobsRunnerStatusMixin):
96
87
  ) # set the cache for the first interview
97
88
  self.total_interviews.append(interview)
98
89
 
90
+ async def run_async(self, cache=None) -> Results:
91
+ if cache is None:
92
+ self.cache = Cache()
93
+ else:
94
+ self.cache = cache
95
+ data = []
96
+ async for result in self.run_async_generator(cache=self.cache):
97
+ data.append(result)
98
+ return Results(survey=self.jobs.survey, data=data)
99
+
100
+ def simple_run(self):
101
+ data = asyncio.run(self.run_async())
102
+ return Results(survey=self.jobs.survey, data=data)
103
+
99
104
  async def _build_interview_task(
100
105
  self,
101
106
  *,
@@ -138,19 +143,21 @@ class JobsRunnerAsyncio(JobsRunnerStatusMixin):
138
143
 
139
144
  prompt_dictionary = {}
140
145
  for answer_key_name in answer_key_names:
141
- prompt_dictionary[
142
- answer_key_name + "_user_prompt"
143
- ] = question_name_to_prompts[answer_key_name]["user_prompt"]
144
- prompt_dictionary[
145
- answer_key_name + "_system_prompt"
146
- ] = question_name_to_prompts[answer_key_name]["system_prompt"]
146
+ prompt_dictionary[answer_key_name + "_user_prompt"] = (
147
+ question_name_to_prompts[answer_key_name]["user_prompt"]
148
+ )
149
+ prompt_dictionary[answer_key_name + "_system_prompt"] = (
150
+ question_name_to_prompts[answer_key_name]["system_prompt"]
151
+ )
147
152
 
148
153
  raw_model_results_dictionary = {}
149
154
  for result in valid_results:
150
155
  question_name = result["question_name"]
151
- raw_model_results_dictionary[
152
- question_name + "_raw_model_response"
153
- ] = result["raw_model_response"]
156
+ raw_model_results_dictionary[question_name + "_raw_model_response"] = (
157
+ result["raw_model_response"]
158
+ )
159
+
160
+ from edsl.results.Result import Result
154
161
 
155
162
  result = Result(
156
163
  agent=interview.agent,
@@ -180,6 +187,8 @@ class JobsRunnerAsyncio(JobsRunnerStatusMixin):
180
187
  print_exceptions: bool = True,
181
188
  ) -> "Coroutine":
182
189
  """Runs a collection of interviews, handling both async and sync contexts."""
190
+ from rich.console import Console
191
+
183
192
  console = Console()
184
193
  self.results = []
185
194
  self.start_time = time.monotonic()
@@ -187,39 +196,15 @@ class JobsRunnerAsyncio(JobsRunnerStatusMixin):
187
196
  self.cache = cache
188
197
  self.sidecar_model = sidecar_model
189
198
 
190
- def generate_table():
191
- return self.status_table(self.results, self.elapsed_time)
199
+ from edsl.results.Results import Results
192
200
 
193
- @contextmanager
194
- def no_op_cm():
195
- """A no-op context manager with a dummy update method."""
196
- yield DummyLive()
197
-
198
- class DummyLive:
199
- def update(self, *args, **kwargs):
200
- """A dummy update method that does nothing."""
201
- pass
202
-
203
- progress_bar_context = (
204
- Live(generate_table(), console=console, refresh_per_second=5)
205
- if progress_bar
206
- else no_op_cm()
207
- )
208
-
209
- with cache as c:
210
- with progress_bar_context as live:
211
-
212
- async def update_progress_bar():
213
- """Updates the progress bar at fixed intervals."""
214
- while True:
215
- live.update(generate_table())
216
- await asyncio.sleep(0.1) # Update interval
217
- if self.completed:
218
- break
201
+ if not progress_bar:
202
+ # print("Running without progress bar")
203
+ with cache as c:
219
204
 
220
205
  async def process_results():
221
206
  """Processes results from interviews."""
222
- async for result in self.run_async(
207
+ async for result in self.run_async_generator(
223
208
  n=n,
224
209
  debug=debug,
225
210
  stop_on_exception=stop_on_exception,
@@ -227,25 +212,76 @@ class JobsRunnerAsyncio(JobsRunnerStatusMixin):
227
212
  sidecar_model=sidecar_model,
228
213
  ):
229
214
  self.results.append(result)
230
- live.update(generate_table())
231
215
  self.completed = True
232
216
 
233
- progress_task = asyncio.create_task(update_progress_bar())
217
+ await asyncio.gather(process_results())
234
218
 
235
- try:
236
- await asyncio.gather(process_results(), progress_task)
237
- except asyncio.CancelledError:
219
+ results = Results(survey=self.jobs.survey, data=self.results)
220
+ else:
221
+ # print("Running with progress bar")
222
+ from rich.live import Live
223
+ from rich.console import Console
224
+
225
+ def generate_table():
226
+ return self.status_table(self.results, self.elapsed_time)
227
+
228
+ @contextmanager
229
+ def no_op_cm():
230
+ """A no-op context manager with a dummy update method."""
231
+ yield DummyLive()
232
+
233
+ class DummyLive:
234
+ def update(self, *args, **kwargs):
235
+ """A dummy update method that does nothing."""
238
236
  pass
239
- finally:
240
- progress_task.cancel() # Cancel the progress_task when process_results is done
241
- await progress_task
242
237
 
243
- await asyncio.sleep(1) # short delay to show the final status
238
+ progress_bar_context = (
239
+ Live(generate_table(), console=console, refresh_per_second=5)
240
+ if progress_bar
241
+ else no_op_cm()
242
+ )
244
243
 
245
- # one more update
246
- live.update(generate_table())
244
+ with cache as c:
245
+ with progress_bar_context as live:
246
+
247
+ async def update_progress_bar():
248
+ """Updates the progress bar at fixed intervals."""
249
+ while True:
250
+ live.update(generate_table())
251
+ await asyncio.sleep(0.00001) # Update interval
252
+ if self.completed:
253
+ break
254
+
255
+ async def process_results():
256
+ """Processes results from interviews."""
257
+ async for result in self.run_async_generator(
258
+ n=n,
259
+ debug=debug,
260
+ stop_on_exception=stop_on_exception,
261
+ cache=c,
262
+ sidecar_model=sidecar_model,
263
+ ):
264
+ self.results.append(result)
265
+ live.update(generate_table())
266
+ self.completed = True
267
+
268
+ progress_task = asyncio.create_task(update_progress_bar())
269
+
270
+ try:
271
+ await asyncio.gather(process_results(), progress_task)
272
+ except asyncio.CancelledError:
273
+ pass
274
+ finally:
275
+ progress_task.cancel() # Cancel the progress_task when process_results is done
276
+ await progress_task
277
+
278
+ await asyncio.sleep(1) # short delay to show the final status
279
+
280
+ # one more update
281
+ live.update(generate_table())
282
+
283
+ results = Results(survey=self.jobs.survey, data=self.results)
247
284
 
248
- results = Results(survey=self.jobs.survey, data=self.results)
249
285
  task_history = TaskHistory(self.total_interviews, include_traceback=False)
250
286
  results.task_history = task_history
251
287
 
@@ -259,6 +295,8 @@ class JobsRunnerAsyncio(JobsRunnerStatusMixin):
259
295
  for interview in self.total_interviews
260
296
  if interview.has_exceptions
261
297
  ]
298
+ from edsl.jobs.Jobs import Jobs
299
+
262
300
  results.failed_jobs = Jobs.from_interviews(
263
301
  [interview for interview in failed_interviews]
264
302
  )
@@ -81,7 +81,7 @@ class JobsRunnerStatusData:
81
81
  >>> completed_tasks = []
82
82
  >>> elapsed_time = 0
83
83
  >>> JobsRunnerStatusData().generate_status_summary(completed_tasks, elapsed_time, interviews)
84
- {'Elapsed time': '0.0 sec.', 'Total interviews requested': '1 ', 'Completed interviews': '0 ', 'Percent complete': '0 %', 'Average time per interview': 'NA', 'Task remaining': '1 ', 'Estimated time remaining': 'NA', 'model_queues': [{'model_name': 'gpt-4-1106-preview', 'TPM_limit_k': 1600.0, 'RPM_limit_k': 8.0, 'num_tasks_waiting': 0, 'token_usage_info': [{'cache_status': 'new_token_usage', 'details': [{'type': 'prompt_tokens', 'tokens': 0}, {'type': 'completion_tokens', 'tokens': 0}], 'cost': '$0.00000'}, {'cache_status': 'cached_token_usage', 'details': [{'type': 'prompt_tokens', 'tokens': 0}, {'type': 'completion_tokens', 'tokens': 0}], 'cost': '$0.00000'}]}]}
84
+ {'Elapsed time': '0.0 sec.', 'Total interviews requested': '1 ', 'Completed interviews': '0 ', 'Percent complete': '0 %', 'Average time per interview': 'NA', 'Task remaining': '1 ', 'Estimated time remaining': 'NA', 'model_queues': [{'model_name': '...', 'TPM_limit_k': ..., 'RPM_limit_k': ..., 'num_tasks_waiting': 0, 'token_usage_info': [{'cache_status': 'new_token_usage', 'details': [{'type': 'prompt_tokens', 'tokens': 0}, {'type': 'completion_tokens', 'tokens': 0}], 'cost': '$0.00000'}, {'cache_status': 'cached_token_usage', 'details': [{'type': 'prompt_tokens', 'tokens': 0}, {'type': 'completion_tokens', 'tokens': 0}], 'cost': '$0.00000'}]}]}
85
85
  """
86
86
 
87
87
  models_to_tokens = defaultdict(InterviewTokenUsage)
@@ -176,7 +176,7 @@ class JobsRunnerStatusData:
176
176
  >>> model = interviews[0].model
177
177
  >>> num_waiting = 0
178
178
  >>> JobsRunnerStatusData()._get_model_info(model, num_waiting, models_to_tokens)
179
- {'model_name': 'gpt-4-1106-preview', 'TPM_limit_k': 1600.0, 'RPM_limit_k': 8.0, 'num_tasks_waiting': 0, 'token_usage_info': [{'cache_status': 'new_token_usage', 'details': [{'type': 'prompt_tokens', 'tokens': 0}, {'type': 'completion_tokens', 'tokens': 0}], 'cost': '$0.00000'}, {'cache_status': 'cached_token_usage', 'details': [{'type': 'prompt_tokens', 'tokens': 0}, {'type': 'completion_tokens', 'tokens': 0}], 'cost': '$0.00000'}]}
179
+ {'model_name': 'gpt-4-1106-preview', 'TPM_limit_k': ..., 'RPM_limit_k': ..., 'num_tasks_waiting': 0, 'token_usage_info': [{'cache_status': 'new_token_usage', 'details': [{'type': 'prompt_tokens', 'tokens': 0}, {'type': 'completion_tokens', 'tokens': 0}], 'cost': '$0.00000'}, {'cache_status': 'cached_token_usage', 'details': [{'type': 'prompt_tokens', 'tokens': 0}, {'type': 'completion_tokens', 'tokens': 0}], 'cost': '$0.00000'}]}
180
180
  """
181
181
 
182
182
  prices = get_token_pricing(model.model)
@@ -234,4 +234,4 @@ class JobsRunnerStatusData:
234
234
  if __name__ == "__main__":
235
235
  import doctest
236
236
 
237
- doctest.testmod()
237
+ doctest.testmod(optionflags=doctest.ELLIPSIS)
@@ -1,7 +1,7 @@
1
1
  import asyncio
2
2
  from typing import Callable, Union, List
3
3
  from collections import UserList, UserDict
4
-
4
+ import time
5
5
 
6
6
  from edsl.jobs.buckets import ModelBuckets
7
7
  from edsl.exceptions import InterviewErrorPriorTaskCanceled
@@ -132,7 +132,7 @@ class QuestionTaskCreator(UserList):
132
132
  self.waiting = True
133
133
  self.task_status = TaskStatus.WAITING_FOR_REQUEST_CAPACITY
134
134
 
135
- await self.requests_bucket.get_tokens(1)
135
+ await self.tokens_bucket.get_tokens(1)
136
136
 
137
137
  self.task_status = TaskStatus.API_CALL_IN_PROGRESS
138
138
  try:
@@ -151,6 +151,8 @@ class QuestionTaskCreator(UserList):
151
151
  self.requests_bucket.add_tokens(1)
152
152
  self.from_cache = True
153
153
 
154
+ _ = results.pop("cached_response", None)
155
+
154
156
  tracker = self.cached_token_usage if self.from_cache else self.new_token_usage
155
157
 
156
158
  # TODO: This is hacky. The 'func' call should return an object that definitely has a 'usage' key.
@@ -1,8 +1,5 @@
1
1
  from edsl.jobs.tasks.task_status_enum import TaskStatus
2
- from matplotlib import pyplot as plt
3
2
  from typing import List, Optional
4
-
5
- import matplotlib.pyplot as plt
6
3
  from io import BytesIO
7
4
  import base64
8
5
 
@@ -75,6 +72,8 @@ class TaskHistory:
75
72
 
76
73
  def plot_completion_times(self):
77
74
  """Plot the completion times for each task."""
75
+ import matplotlib.pyplot as plt
76
+
78
77
  updates = self.get_updates()
79
78
 
80
79
  elapsed = [update.max_time - update.min_time for update in updates]
@@ -126,6 +125,8 @@ class TaskHistory:
126
125
  rows = int(len(TaskStatus) ** 0.5) + 1
127
126
  cols = (len(TaskStatus) + rows - 1) // rows # Ensure all plots fit
128
127
 
128
+ import matplotlib.pyplot as plt
129
+
129
130
  fig, axes = plt.subplots(rows, cols, figsize=(15, 10))
130
131
  axes = axes.flatten() # Flatten in case of a single row/column
131
132