edsl 0.1.40.dev2__py3-none-any.whl → 0.1.42__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 (59) hide show
  1. edsl/__init__.py +1 -0
  2. edsl/__version__.py +1 -1
  3. edsl/agents/Agent.py +1 -1
  4. edsl/agents/Invigilator.py +6 -4
  5. edsl/agents/InvigilatorBase.py +2 -1
  6. edsl/agents/QuestionTemplateReplacementsBuilder.py +7 -2
  7. edsl/coop/coop.py +37 -2
  8. edsl/data/Cache.py +7 -0
  9. edsl/data/RemoteCacheSync.py +16 -16
  10. edsl/enums.py +3 -0
  11. edsl/exceptions/jobs.py +1 -9
  12. edsl/exceptions/language_models.py +8 -4
  13. edsl/exceptions/questions.py +8 -11
  14. edsl/inference_services/DeepSeekService.py +18 -0
  15. edsl/inference_services/registry.py +2 -0
  16. edsl/jobs/AnswerQuestionFunctionConstructor.py +1 -1
  17. edsl/jobs/Jobs.py +42 -34
  18. edsl/jobs/JobsPrompts.py +11 -1
  19. edsl/jobs/JobsRemoteInferenceHandler.py +1 -0
  20. edsl/jobs/JobsRemoteInferenceLogger.py +1 -1
  21. edsl/jobs/interviews/Interview.py +2 -6
  22. edsl/jobs/interviews/InterviewExceptionEntry.py +14 -4
  23. edsl/jobs/loggers/HTMLTableJobLogger.py +6 -1
  24. edsl/jobs/results_exceptions_handler.py +2 -7
  25. edsl/jobs/runners/JobsRunnerAsyncio.py +18 -6
  26. edsl/jobs/runners/JobsRunnerStatus.py +2 -1
  27. edsl/jobs/tasks/TaskHistory.py +49 -17
  28. edsl/language_models/LanguageModel.py +7 -4
  29. edsl/language_models/ModelList.py +1 -1
  30. edsl/language_models/key_management/KeyLookupBuilder.py +7 -3
  31. edsl/language_models/model.py +49 -0
  32. edsl/questions/QuestionBudget.py +2 -2
  33. edsl/questions/QuestionDict.py +343 -0
  34. edsl/questions/QuestionExtract.py +1 -1
  35. edsl/questions/__init__.py +1 -0
  36. edsl/questions/answer_validator_mixin.py +29 -0
  37. edsl/questions/derived/QuestionLinearScale.py +1 -1
  38. edsl/questions/descriptors.py +49 -5
  39. edsl/questions/question_registry.py +1 -1
  40. edsl/questions/templates/dict/__init__.py +0 -0
  41. edsl/questions/templates/dict/answering_instructions.jinja +21 -0
  42. edsl/questions/templates/dict/question_presentation.jinja +1 -0
  43. edsl/results/Result.py +25 -3
  44. edsl/results/Results.py +17 -5
  45. edsl/scenarios/FileStore.py +32 -0
  46. edsl/scenarios/PdfExtractor.py +3 -6
  47. edsl/scenarios/Scenario.py +2 -1
  48. edsl/scenarios/handlers/csv.py +11 -0
  49. edsl/surveys/Survey.py +5 -1
  50. edsl/templates/error_reporting/base.html +2 -4
  51. edsl/templates/error_reporting/exceptions_table.html +35 -0
  52. edsl/templates/error_reporting/interview_details.html +67 -53
  53. edsl/templates/error_reporting/interviews.html +4 -17
  54. edsl/templates/error_reporting/overview.html +31 -5
  55. edsl/templates/error_reporting/performance_plot.html +1 -1
  56. {edsl-0.1.40.dev2.dist-info → edsl-0.1.42.dist-info}/METADATA +1 -1
  57. {edsl-0.1.40.dev2.dist-info → edsl-0.1.42.dist-info}/RECORD +59 -53
  58. {edsl-0.1.40.dev2.dist-info → edsl-0.1.42.dist-info}/LICENSE +0 -0
  59. {edsl-0.1.40.dev2.dist-info → edsl-0.1.42.dist-info}/WHEEL +0 -0
@@ -1,5 +1,6 @@
1
1
  import traceback
2
2
  import datetime
3
+ from edsl.agents.InvigilatorBase import InvigilatorBase
3
4
 
4
5
 
5
6
  class InterviewExceptionEntry:
@@ -9,7 +10,7 @@ class InterviewExceptionEntry:
9
10
  self,
10
11
  *,
11
12
  exception: Exception,
12
- invigilator: "Invigilator",
13
+ invigilator: "InvigilatorBase",
13
14
  traceback_format="text",
14
15
  answers=None,
15
16
  ):
@@ -20,6 +21,8 @@ class InterviewExceptionEntry:
20
21
  self.traceback_format = traceback_format
21
22
  self.answers = answers
22
23
 
24
+ # breakpoint()
25
+
23
26
  @property
24
27
  def question_type(self):
25
28
  # return self.failed_question.question.question_type
@@ -163,12 +166,16 @@ class InterviewExceptionEntry:
163
166
  >>> entry = InterviewExceptionEntry.example()
164
167
  >>> _ = entry.to_dict()
165
168
  """
166
- return {
169
+ invigilator = (
170
+ self.invigilator.to_dict() if self.invigilator is not None else None
171
+ )
172
+ d = {
167
173
  "exception": self.serialize_exception(self.exception),
168
174
  "time": self.time,
169
175
  "traceback": self.traceback,
170
- "invigilator": self.invigilator.to_dict(),
176
+ "invigilator": invigilator,
171
177
  }
178
+ return d
172
179
 
173
180
  @classmethod
174
181
  def from_dict(cls, data: dict) -> "InterviewExceptionEntry":
@@ -176,7 +183,10 @@ class InterviewExceptionEntry:
176
183
  from edsl.agents.Invigilator import InvigilatorAI
177
184
 
178
185
  exception = cls.deserialize_exception(data["exception"])
179
- invigilator = InvigilatorAI.from_dict(data["invigilator"])
186
+ if data["invigilator"] is None:
187
+ invigilator = None
188
+ else:
189
+ invigilator = InvigilatorAI.from_dict(data["invigilator"])
180
190
  return cls(exception=exception, invigilator=invigilator)
181
191
 
182
192
 
@@ -9,7 +9,8 @@ from edsl.jobs.jobs_status_enums import JobsStatus
9
9
  class HTMLTableJobLogger(JobLogger):
10
10
  def __init__(self, verbose=True, theme="auto", **kwargs):
11
11
  super().__init__(verbose=verbose)
12
- self.display_handle = display(HTML(""), display_id=True)
12
+ self.display_handle = display(HTML(""), display_id=True) if verbose else None
13
+ #self.display_handle = display(HTML(""), display_id=True)
13
14
  self.current_message = None
14
15
  self.log_id = str(uuid.uuid4())
15
16
  self.is_expanded = True
@@ -22,6 +23,9 @@ class HTMLTableJobLogger(JobLogger):
22
23
 
23
24
  def _init_css(self):
24
25
  """Initialize the CSS styles with enhanced theme support"""
26
+ if not self.verbose:
27
+ return None
28
+
25
29
  css = """
26
30
  <style>
27
31
  /* Base theme variables */
@@ -217,6 +221,7 @@ class HTMLTableJobLogger(JobLogger):
217
221
  }});
218
222
  </script>
219
223
  """
224
+
220
225
 
221
226
  display(HTML(css + init_script))
222
227
 
@@ -66,9 +66,7 @@ class ResultsExceptionsHandler:
66
66
 
67
67
  def _generate_error_message(self, indices) -> str:
68
68
  """Generate appropriate error message based on number of exceptions."""
69
- msg = f"Exceptions were raised in {len(indices)} interviews.\n"
70
- if len(indices) > 5:
71
- msg += f"Exceptions were raised in the following interviews: {indices}.\n"
69
+ msg = f"Exceptions were raised.\n"
72
70
  return msg
73
71
 
74
72
  def handle_exceptions(self) -> None:
@@ -84,7 +82,6 @@ class ResultsExceptionsHandler:
84
82
 
85
83
  # Generate HTML report
86
84
  filepath = self.results.task_history.html(
87
- cta="Open report to see details.",
88
85
  open_in_browser=self.open_in_browser,
89
86
  return_link=True,
90
87
  )
@@ -92,7 +89,5 @@ class ResultsExceptionsHandler:
92
89
  # Handle remote logging if enabled
93
90
  if self.remote_logging:
94
91
  filestore = HTMLFileStore(filepath)
95
- coop_details = filestore.push(description="Error report")
92
+ coop_details = filestore.push(description="Exceptions Report")
96
93
  print(coop_details)
97
-
98
- print("Also see: https://docs.expectedparrot.com/en/latest/exceptions.html")
@@ -44,7 +44,16 @@ class JobsRunnerAsyncio:
44
44
  data.append(result)
45
45
  task_history.add_interview(interview)
46
46
 
47
- return Results(survey=self.jobs.survey, task_history=task_history, data=data)
47
+ results = Results(survey=self.jobs.survey, task_history=task_history, data=data)
48
+
49
+ relevant_cache = results.relevant_cache(self.environment.cache)
50
+
51
+ return Results(
52
+ survey=self.jobs.survey,
53
+ task_history=task_history,
54
+ data=data,
55
+ cache=relevant_cache,
56
+ )
48
57
 
49
58
  def simple_run(self):
50
59
  data = asyncio.run(self.run_async())
@@ -93,16 +102,16 @@ class JobsRunnerAsyncio:
93
102
 
94
103
  self.completed = True
95
104
 
96
- def run_progress_bar(stop_event) -> None:
105
+ def run_progress_bar(stop_event, jobs_runner_status) -> None:
97
106
  """Runs the progress bar in a separate thread."""
98
- self.jobs_runner_status.update_progress(stop_event)
107
+ jobs_runner_status.update_progress(stop_event)
99
108
 
100
109
  def set_up_progress_bar(progress_bar: bool, jobs_runner_status):
101
110
  progress_thread = None
102
111
  if progress_bar and jobs_runner_status.has_ep_api_key():
103
112
  jobs_runner_status.setup()
104
113
  progress_thread = threading.Thread(
105
- target=run_progress_bar, args=(stop_event,)
114
+ target=run_progress_bar, args=(stop_event, jobs_runner_status)
106
115
  )
107
116
  progress_thread.start()
108
117
  elif progress_bar:
@@ -115,8 +124,9 @@ class JobsRunnerAsyncio:
115
124
  survey=self.jobs.survey,
116
125
  data=[],
117
126
  task_history=TaskHistory(),
118
- cache=self.environment.cache.new_entries_cache(),
127
+ # cache=self.environment.cache.new_entries_cache(),
119
128
  )
129
+
120
130
  stop_event = threading.Event()
121
131
  progress_thread = set_up_progress_bar(
122
132
  parameters.progress_bar, run_config.environment.jobs_runner_status
@@ -140,7 +150,9 @@ class JobsRunnerAsyncio:
140
150
  if exception_to_raise:
141
151
  raise exception_to_raise
142
152
 
143
- results.cache = self.environment.cache.new_entries_cache()
153
+ relevant_cache = results.relevant_cache(self.environment.cache)
154
+ results.cache = relevant_cache
155
+ # breakpoint()
144
156
  results.bucket_collection = self.environment.bucket_collection
145
157
 
146
158
  from edsl.jobs.results_exceptions_handler import ResultsExceptionsHandler
@@ -148,7 +148,8 @@ class JobsRunnerStatusBase(ABC):
148
148
  }
149
149
 
150
150
  model_queues = {}
151
- for model, bucket in self.jobs_runner.bucket_collection.items():
151
+ # for model, bucket in self.jobs_runner.bucket_collection.items():
152
+ for model, bucket in self.jobs_runner.environment.bucket_collection.items():
152
153
  model_name = model.model
153
154
  model_queues[model_name] = {
154
155
  "language_model_name": model_name,
@@ -264,9 +264,27 @@ class TaskHistory(RepresentationMixin):
264
264
  js = env.joinpath("report.js").read_text()
265
265
  return js
266
266
 
267
+ @property
268
+ def exceptions_table(self) -> dict:
269
+ """Return a dictionary of exceptions organized by type, service, model, and question name."""
270
+ exceptions_table = {}
271
+ for interview in self.total_interviews:
272
+ for question_name, exceptions in interview.exceptions.items():
273
+ for exception in exceptions:
274
+ key = (
275
+ exception.exception.__class__.__name__, # Exception type
276
+ interview.model._inference_service_, # Service
277
+ interview.model.model, # Model
278
+ question_name # Question name
279
+ )
280
+ if key not in exceptions_table:
281
+ exceptions_table[key] = 0
282
+ exceptions_table[key] += 1
283
+ return exceptions_table
284
+
267
285
  @property
268
286
  def exceptions_by_type(self) -> dict:
269
- """Return a dictionary of exceptions by type."""
287
+ """Return a dictionary of exceptions tallied by type."""
270
288
  exceptions_by_type = {}
271
289
  for interview in self.total_interviews:
272
290
  for question_name, exceptions in interview.exceptions.items():
@@ -324,6 +342,27 @@ class TaskHistory(RepresentationMixin):
324
342
  }
325
343
  return sorted_exceptions_by_question_name
326
344
 
345
+ # @property
346
+ # def exceptions_by_model(self) -> dict:
347
+ # """Return a dictionary of exceptions tallied by model and question name."""
348
+ # exceptions_by_model = {}
349
+ # for interview in self.total_interviews:
350
+ # model = interview.model.model
351
+ # service = interview.model._inference_service_
352
+ # if (service, model) not in exceptions_by_model:
353
+ # exceptions_by_model[(service, model)] = 0
354
+ # if interview.exceptions != {}:
355
+ # exceptions_by_model[(service, model)] += len(interview.exceptions)
356
+
357
+ # # sort the exceptions by model
358
+ # sorted_exceptions_by_model = {
359
+ # k: v
360
+ # for k, v in sorted(
361
+ # exceptions_by_model.items(), key=lambda item: item[1], reverse=True
362
+ # )
363
+ # }
364
+ # return sorted_exceptions_by_model
365
+
327
366
  @property
328
367
  def exceptions_by_model(self) -> dict:
329
368
  """Return a dictionary of exceptions tallied by model and question name."""
@@ -331,19 +370,12 @@ class TaskHistory(RepresentationMixin):
331
370
  for interview in self.total_interviews:
332
371
  model = interview.model.model
333
372
  service = interview.model._inference_service_
334
- if (service, model) not in exceptions_by_model:
335
- exceptions_by_model[(service, model)] = 0
336
- if interview.exceptions != {}:
337
- exceptions_by_model[(service, model)] += len(interview.exceptions)
338
-
339
- # sort the exceptions by model
340
- sorted_exceptions_by_model = {
341
- k: v
342
- for k, v in sorted(
343
- exceptions_by_model.items(), key=lambda item: item[1], reverse=True
344
- )
345
- }
346
- return sorted_exceptions_by_model
373
+ for question_name, exceptions in interview.exceptions.items():
374
+ key = (service, model, question_name)
375
+ if key not in exceptions_by_model:
376
+ exceptions_by_model[key] = 0
377
+ exceptions_by_model[key] += len(exceptions)
378
+ return exceptions_by_model
347
379
 
348
380
  def generate_html_report(self, css: Optional[str], include_plot=False):
349
381
  if include_plot:
@@ -372,6 +404,7 @@ class TaskHistory(RepresentationMixin):
372
404
  javascript=self.javascript(),
373
405
  num_exceptions=len(self.exceptions),
374
406
  performance_plot_html=performance_plot_html,
407
+ exceptions_table=self.exceptions_table,
375
408
  exceptions_by_type=self.exceptions_by_type,
376
409
  exceptions_by_question_name=self.exceptions_by_question_name,
377
410
  exceptions_by_model=self.exceptions_by_model,
@@ -386,11 +419,10 @@ class TaskHistory(RepresentationMixin):
386
419
  filename: Optional[str] = None,
387
420
  return_link=False,
388
421
  css=None,
389
- cta="Open Report in New Tab",
422
+ cta="\nClick to open the report in a new tab\n",
390
423
  open_in_browser=False,
391
424
  ):
392
425
  """Return an HTML report."""
393
-
394
426
  from IPython.display import display, HTML
395
427
  import tempfile
396
428
  import os
@@ -419,7 +451,7 @@ class TaskHistory(RepresentationMixin):
419
451
  html_link = f'<a href="{html_url}" target="_blank">{cta}</a>'
420
452
  display(HTML(html_link))
421
453
  escaped_output = html.escape(output)
422
- iframe = f""""
454
+ iframe = f"""
423
455
  <iframe srcdoc="{ escaped_output }" style="width: 800px; height: 600px;"></iframe>
424
456
  """
425
457
  display(HTML(iframe))
@@ -244,7 +244,7 @@ class LanguageModel(
244
244
 
245
245
  >>> m = LanguageModel.example()
246
246
  >>> hash(m)
247
- 1811901442659237949
247
+ 325654563661254408
248
248
  """
249
249
  from edsl.utilities.utilities import dict_hash
250
250
 
@@ -495,11 +495,12 @@ class LanguageModel(
495
495
 
496
496
  >>> m = LanguageModel.example()
497
497
  >>> m.to_dict()
498
- {'model': '...', 'parameters': {'temperature': ..., 'max_tokens': ..., 'top_p': ..., 'frequency_penalty': ..., 'presence_penalty': ..., 'logprobs': False, 'top_logprobs': ...}, 'edsl_version': '...', 'edsl_class_name': 'LanguageModel'}
498
+ {'model': '...', 'parameters': {'temperature': ..., 'max_tokens': ..., 'top_p': ..., 'frequency_penalty': ..., 'presence_penalty': ..., 'logprobs': False, 'top_logprobs': ...}, 'inference_service': 'openai', 'edsl_version': '...', 'edsl_class_name': 'LanguageModel'}
499
499
  """
500
500
  d = {
501
501
  "model": self.model,
502
502
  "parameters": self.parameters,
503
+ "inference_service": self._inference_service_,
503
504
  }
504
505
  if add_edsl_version:
505
506
  from edsl import __version__
@@ -511,7 +512,10 @@ class LanguageModel(
511
512
  @classmethod
512
513
  @remove_edsl_version
513
514
  def from_dict(cls, data: dict) -> Type[LanguageModel]:
514
- """Convert dictionary to a LanguageModel child instance."""
515
+ """Convert dictionary to a LanguageModel child instance.
516
+
517
+ NB: This method does not use the stores inference_service but rather just fetches a model class based on the name.
518
+ """
515
519
  from edsl.language_models.model import get_model_class
516
520
 
517
521
  model_class = get_model_class(data["model"])
@@ -558,7 +562,6 @@ class LanguageModel(
558
562
  >>> m = LanguageModel.example(test_model = True, canned_response = "WOWZA!", throw_exception = True)
559
563
  >>> r = q.by(m).run(cache = False, disable_remote_cache = True, disable_remote_inference = True, print_exceptions = True)
560
564
  Exception report saved to ...
561
- Also see: ...
562
565
  """
563
566
  from edsl.language_models.model import Model
564
567
 
@@ -60,7 +60,7 @@ class ModelList(Base, UserList):
60
60
 
61
61
  sl = ScenarioList()
62
62
  for model in self:
63
- d = {"model": model.model}
63
+ d = {"model": model.model, "inference_service": model._inference_service_}
64
64
  d.update(model.parameters)
65
65
  sl.append(Scenario(d))
66
66
  return sl
@@ -36,7 +36,7 @@ class KeyLookupBuilder:
36
36
 
37
37
  >>> builder = KeyLookupBuilder(fetch_order=("config", "env"))
38
38
  >>> builder.DEFAULT_RPM
39
- 10
39
+ 100
40
40
  >>> builder.DEFAULT_TPM
41
41
  2000000
42
42
  >>> builder.fetch_order
@@ -54,8 +54,12 @@ class KeyLookupBuilder:
54
54
  ('openai', 'rpm')
55
55
  """
56
56
 
57
- DEFAULT_RPM = 10
58
- DEFAULT_TPM = 2000000
57
+ # DEFAULT_RPM = 10
58
+ # DEFAULT_TPM = 2000000
59
+ from edsl.config import CONFIG
60
+
61
+ DEFAULT_RPM = int(CONFIG.get("EDSL_SERVICE_RPM_BASELINE"))
62
+ DEFAULT_TPM = int(CONFIG.get("EDSL_SERVICE_TPM_BASELINE"))
59
63
 
60
64
  def __init__(self, fetch_order: Optional[tuple[str]] = None):
61
65
  if fetch_order is None:
@@ -233,6 +233,55 @@ class Model(metaclass=Meta):
233
233
  print("OK!")
234
234
  print("\n")
235
235
 
236
+ @classmethod
237
+ def check_working_models(
238
+ cls,
239
+ service: Optional[str] = None,
240
+ works_with_text: Optional[bool] = None,
241
+ works_with_images: Optional[bool] = None,
242
+ ) -> list[dict]:
243
+ from edsl.coop import Coop
244
+
245
+ c = Coop()
246
+ working_models = c.fetch_working_models()
247
+
248
+ if service is not None:
249
+ working_models = [m for m in working_models if m["service"] == service]
250
+ if works_with_text is not None:
251
+ working_models = [
252
+ m for m in working_models if m["works_with_text"] == works_with_text
253
+ ]
254
+ if works_with_images is not None:
255
+ working_models = [
256
+ m for m in working_models if m["works_with_images"] == works_with_images
257
+ ]
258
+
259
+ if len(working_models) == 0:
260
+ return []
261
+
262
+ else:
263
+ return PrettyList(
264
+ [
265
+ [
266
+ m["service"],
267
+ m["model"],
268
+ m["works_with_text"],
269
+ m["works_with_images"],
270
+ m["usd_per_1M_input_tokens"],
271
+ m["usd_per_1M_output_tokens"],
272
+ ]
273
+ for m in working_models
274
+ ],
275
+ columns=[
276
+ "Service",
277
+ "Model",
278
+ "Works with text",
279
+ "Works with images",
280
+ "Price per 1M input tokens (USD)",
281
+ "Price per 1M output tokens (USD)",
282
+ ],
283
+ )
284
+
236
285
  @classmethod
237
286
  def example(cls, randomize: bool = False) -> "Model":
238
287
  """
@@ -8,7 +8,7 @@ from edsl.questions.descriptors import IntegerDescriptor, QuestionOptionsDescrip
8
8
  from edsl.questions.response_validator_abc import ResponseValidatorABC
9
9
 
10
10
 
11
- class BudgewResponseValidator(ResponseValidatorABC):
11
+ class BudgetResponseValidator(ResponseValidatorABC):
12
12
  valid_examples = []
13
13
 
14
14
  invalid_examples = []
@@ -64,7 +64,7 @@ class QuestionBudget(QuestionBase):
64
64
  budget_sum: int = IntegerDescriptor(none_allowed=False)
65
65
  question_options: list[str] = QuestionOptionsDescriptor(q_budget=True)
66
66
  _response_model = None
67
- response_validator_class = BudgewResponseValidator
67
+ response_validator_class = BudgetResponseValidator
68
68
 
69
69
  def __init__(
70
70
  self,