edsl 0.1.50__py3-none-any.whl → 0.1.52__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/__init__.py +45 -34
  2. edsl/__version__.py +1 -1
  3. edsl/base/base_exception.py +2 -2
  4. edsl/buckets/bucket_collection.py +1 -1
  5. edsl/buckets/exceptions.py +32 -0
  6. edsl/buckets/token_bucket_api.py +26 -10
  7. edsl/caching/cache.py +5 -2
  8. edsl/caching/remote_cache_sync.py +5 -5
  9. edsl/caching/sql_dict.py +12 -11
  10. edsl/config/__init__.py +1 -1
  11. edsl/config/config_class.py +4 -2
  12. edsl/conversation/Conversation.py +9 -5
  13. edsl/conversation/car_buying.py +1 -3
  14. edsl/conversation/mug_negotiation.py +2 -6
  15. edsl/coop/__init__.py +11 -8
  16. edsl/coop/coop.py +15 -13
  17. edsl/coop/coop_functions.py +1 -1
  18. edsl/coop/ep_key_handling.py +1 -1
  19. edsl/coop/price_fetcher.py +2 -2
  20. edsl/coop/utils.py +2 -2
  21. edsl/dataset/dataset.py +144 -63
  22. edsl/dataset/dataset_operations_mixin.py +14 -6
  23. edsl/dataset/dataset_tree.py +3 -3
  24. edsl/dataset/display/table_renderers.py +6 -3
  25. edsl/dataset/file_exports.py +4 -4
  26. edsl/dataset/r/ggplot.py +3 -3
  27. edsl/inference_services/available_model_fetcher.py +2 -2
  28. edsl/inference_services/data_structures.py +5 -5
  29. edsl/inference_services/inference_service_abc.py +1 -1
  30. edsl/inference_services/inference_services_collection.py +1 -1
  31. edsl/inference_services/service_availability.py +3 -3
  32. edsl/inference_services/services/azure_ai.py +3 -3
  33. edsl/inference_services/services/google_service.py +1 -1
  34. edsl/inference_services/services/test_service.py +1 -1
  35. edsl/instructions/change_instruction.py +5 -4
  36. edsl/instructions/instruction.py +1 -0
  37. edsl/instructions/instruction_collection.py +5 -4
  38. edsl/instructions/instruction_handler.py +10 -8
  39. edsl/interviews/answering_function.py +20 -21
  40. edsl/interviews/exception_tracking.py +3 -2
  41. edsl/interviews/interview.py +1 -1
  42. edsl/interviews/interview_status_dictionary.py +1 -1
  43. edsl/interviews/interview_task_manager.py +7 -4
  44. edsl/interviews/request_token_estimator.py +3 -2
  45. edsl/interviews/statistics.py +2 -2
  46. edsl/invigilators/invigilators.py +34 -6
  47. edsl/jobs/__init__.py +39 -2
  48. edsl/jobs/async_interview_runner.py +1 -1
  49. edsl/jobs/check_survey_scenario_compatibility.py +5 -5
  50. edsl/jobs/data_structures.py +2 -2
  51. edsl/jobs/html_table_job_logger.py +494 -257
  52. edsl/jobs/jobs.py +2 -2
  53. edsl/jobs/jobs_checks.py +5 -5
  54. edsl/jobs/jobs_component_constructor.py +2 -2
  55. edsl/jobs/jobs_pricing_estimation.py +1 -1
  56. edsl/jobs/jobs_runner_asyncio.py +2 -2
  57. edsl/jobs/jobs_status_enums.py +1 -0
  58. edsl/jobs/remote_inference.py +47 -13
  59. edsl/jobs/results_exceptions_handler.py +2 -2
  60. edsl/language_models/language_model.py +151 -145
  61. edsl/notebooks/__init__.py +24 -1
  62. edsl/notebooks/exceptions.py +82 -0
  63. edsl/notebooks/notebook.py +7 -3
  64. edsl/notebooks/notebook_to_latex.py +1 -1
  65. edsl/prompts/__init__.py +23 -2
  66. edsl/prompts/prompt.py +1 -1
  67. edsl/questions/__init__.py +4 -4
  68. edsl/questions/answer_validator_mixin.py +0 -5
  69. edsl/questions/compose_questions.py +2 -2
  70. edsl/questions/descriptors.py +1 -1
  71. edsl/questions/question_base.py +32 -3
  72. edsl/questions/question_base_prompts_mixin.py +4 -4
  73. edsl/questions/question_budget.py +503 -102
  74. edsl/questions/question_check_box.py +658 -156
  75. edsl/questions/question_dict.py +176 -2
  76. edsl/questions/question_extract.py +401 -61
  77. edsl/questions/question_free_text.py +77 -9
  78. edsl/questions/question_functional.py +118 -9
  79. edsl/questions/{derived/question_likert_five.py → question_likert_five.py} +2 -2
  80. edsl/questions/{derived/question_linear_scale.py → question_linear_scale.py} +3 -4
  81. edsl/questions/question_list.py +246 -26
  82. edsl/questions/question_matrix.py +586 -73
  83. edsl/questions/question_multiple_choice.py +213 -47
  84. edsl/questions/question_numerical.py +360 -29
  85. edsl/questions/question_rank.py +401 -124
  86. edsl/questions/question_registry.py +3 -3
  87. edsl/questions/{derived/question_top_k.py → question_top_k.py} +3 -3
  88. edsl/questions/{derived/question_yes_no.py → question_yes_no.py} +3 -4
  89. edsl/questions/register_questions_meta.py +2 -1
  90. edsl/questions/response_validator_abc.py +6 -2
  91. edsl/questions/response_validator_factory.py +10 -12
  92. edsl/results/report.py +1 -1
  93. edsl/results/result.py +7 -4
  94. edsl/results/results.py +500 -271
  95. edsl/results/results_selector.py +2 -2
  96. edsl/scenarios/construct_download_link.py +3 -3
  97. edsl/scenarios/scenario.py +1 -2
  98. edsl/scenarios/scenario_list.py +41 -23
  99. edsl/surveys/survey_css.py +3 -3
  100. edsl/surveys/survey_simulator.py +2 -1
  101. edsl/tasks/__init__.py +22 -2
  102. edsl/tasks/exceptions.py +72 -0
  103. edsl/tasks/task_history.py +48 -11
  104. edsl/templates/error_reporting/base.html +37 -4
  105. edsl/templates/error_reporting/exceptions_table.html +105 -33
  106. edsl/templates/error_reporting/interview_details.html +130 -126
  107. edsl/templates/error_reporting/overview.html +21 -25
  108. edsl/templates/error_reporting/report.css +215 -46
  109. edsl/templates/error_reporting/report.js +122 -20
  110. edsl/tokens/__init__.py +27 -1
  111. edsl/tokens/exceptions.py +37 -0
  112. edsl/tokens/interview_token_usage.py +3 -2
  113. edsl/tokens/token_usage.py +4 -3
  114. {edsl-0.1.50.dist-info → edsl-0.1.52.dist-info}/METADATA +1 -1
  115. {edsl-0.1.50.dist-info → edsl-0.1.52.dist-info}/RECORD +118 -116
  116. edsl/questions/derived/__init__.py +0 -0
  117. {edsl-0.1.50.dist-info → edsl-0.1.52.dist-info}/LICENSE +0 -0
  118. {edsl-0.1.50.dist-info → edsl-0.1.52.dist-info}/WHEEL +0 -0
  119. {edsl-0.1.50.dist-info → edsl-0.1.52.dist-info}/entry_points.txt +0 -0
@@ -106,7 +106,7 @@ class Selector:
106
106
  new_data = self._fetch_data(to_fetch)
107
107
  except ResultsColumnNotFoundError as e:
108
108
  # Check is_notebook with explicit import to ensure mock works
109
- from edsl.utilities import is_notebook as is_notebook_check
109
+ from ..utilities import is_notebook as is_notebook_check
110
110
  if is_notebook_check():
111
111
  print("Error:", e, file=sys.stderr)
112
112
  return None
@@ -114,7 +114,7 @@ class Selector:
114
114
  raise e
115
115
 
116
116
  # Import Dataset here to avoid circular import issues
117
- from edsl.dataset import Dataset
117
+ from ..dataset import Dataset
118
118
  return Dataset(new_data)
119
119
 
120
120
  def _normalize_columns(self, columns: Union[str, List[str]]) -> Tuple[str, ...]:
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
- from typing import TYPE_CHECKING, Optional
4
+ from typing import TYPE_CHECKING, Optional, List
5
5
 
6
6
  if TYPE_CHECKING:
7
7
  from ..display import HTML
@@ -104,8 +104,8 @@ class ConstructDownloadLink:
104
104
 
105
105
  def create_multiple_links(
106
106
  self,
107
- files: list[FileStore],
108
- custom_filenames: Optional[list[str | None]] = None,
107
+ files: List["FileStore"],
108
+ custom_filenames: Optional[List[Optional[str]]] = None,
109
109
  style: Optional[dict] = None,
110
110
  ) -> HTML:
111
111
  """Create multiple download links in a horizontal layout.
@@ -96,8 +96,7 @@ class Scenario(Base, UserDict):
96
96
  data = dict(data)
97
97
  except Exception as e:
98
98
  raise ScenarioError(
99
- f"You must pass in a dictionary to initialize a Scenario. You passed in {data}",
100
- "Exception message:" + str(e),
99
+ f"You must pass in a dictionary to initialize a Scenario. You passed in {data}" + "Exception message:" + str(e),
101
100
  )
102
101
 
103
102
  super().__init__()
@@ -31,7 +31,6 @@ import warnings
31
31
  import csv
32
32
  import random
33
33
  import os
34
- import glob
35
34
  from io import StringIO
36
35
  import inspect
37
36
  from collections import UserList, defaultdict
@@ -488,7 +487,8 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
488
487
  if isinstance(other, Scenario):
489
488
  other = ScenarioList([other])
490
489
  elif not isinstance(other, ScenarioList):
491
- raise TypeError(f"Cannot multiply ScenarioList with {type(other)}")
490
+ from .exceptions import TypeScenarioError
491
+ raise TypeScenarioError(f"Cannot multiply ScenarioList with {type(other)}")
492
492
 
493
493
  new_sl = []
494
494
  for s1, s2 in list(product(self, other)):
@@ -599,7 +599,8 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
599
599
  # Convert to a set (removes duplicates)
600
600
  new_scenario[field_name] = set(values)
601
601
  else:
602
- raise ValueError(f"Invalid output_type: {output_type}. Must be 'string', 'list', or 'set'.")
602
+ from .exceptions import ValueScenarioError
603
+ raise ValueScenarioError(f"Invalid output_type: {output_type}. Must be 'string', 'list', or 'set'.")
603
604
 
604
605
  new_scenarios.append(new_scenario)
605
606
 
@@ -895,11 +896,13 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
895
896
  cls,
896
897
  path: Optional[str] = None,
897
898
  recursive: bool = False,
899
+ key_name: str = "content",
898
900
  ) -> "ScenarioList":
899
- """Create a ScenarioList of FileStore objects from files in a directory.
901
+ """Create a ScenarioList of Scenario objects from files in a directory.
900
902
 
901
- This method scans a directory and creates a FileStore object for each file found,
902
- optionally filtering files based on a wildcard pattern. If no path is provided,
903
+ This method scans a directory and creates a Scenario object for each file found,
904
+ where each Scenario contains a FileStore object under the specified key.
905
+ Optionally filters files based on a wildcard pattern. If no path is provided,
903
906
  the current working directory is used.
904
907
 
905
908
  Args:
@@ -910,25 +913,27 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
910
913
  - "/path/to/directory/*.py" - scans only Python files in the directory
911
914
  - "*.txt" - scans only text files in the current working directory
912
915
  recursive: Whether to scan subdirectories recursively. Defaults to False.
916
+ key_name: The key to use for the FileStore object in each Scenario. Defaults to "content".
913
917
 
914
918
  Returns:
915
- A ScenarioList containing FileStore objects for all matching files.
919
+ A ScenarioList containing Scenario objects for all matching files, where each Scenario
920
+ has a FileStore object under the specified key.
916
921
 
917
922
  Raises:
918
923
  FileNotFoundError: If the specified directory does not exist.
919
924
 
920
925
  Examples:
921
- # Get all files in the current directory
926
+ # Get all files in the current directory with default key "content"
922
927
  sl = ScenarioList.from_directory()
923
928
 
924
- # Get all Python files in a specific directory
925
- sl = ScenarioList.from_directory('*.py')
929
+ # Get all Python files in a specific directory with custom key "python_file"
930
+ sl = ScenarioList.from_directory('*.py', key_name="python_file")
926
931
 
927
932
  # Get all image files in the current directory
928
- sl = ScenarioList.from_directory('*.png')
933
+ sl = ScenarioList.from_directory('*.png', key_name="image")
929
934
 
930
935
  # Get all files recursively including subdirectories
931
- sl = ScenarioList.from_directory(recursive=True)
936
+ sl = ScenarioList.from_directory(recursive=True, key_name="document")
932
937
  """
933
938
  # Handle default case - use current directory
934
939
  if path is None:
@@ -973,7 +978,8 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
973
978
 
974
979
  # Ensure directory exists
975
980
  if not os.path.isdir(directory_path):
976
- raise FileNotFoundError(f"Directory not found: {directory_path}")
981
+ from .exceptions import FileNotFoundScenarioError
982
+ raise FileNotFoundScenarioError(f"Directory not found: {directory_path}")
977
983
 
978
984
  # Create a DirectoryScanner for the directory
979
985
  scanner = DirectoryScanner(directory_path)
@@ -1001,7 +1007,10 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
1001
1007
  example_suffix=example_suffix
1002
1008
  )
1003
1009
 
1004
- return cls(file_stores)
1010
+ # Convert FileStore objects to Scenario objects with the specified key
1011
+ scenarios = [Scenario({key_name: file_store}) for file_store in file_stores]
1012
+
1013
+ return cls(scenarios)
1005
1014
 
1006
1015
  @classmethod
1007
1016
  def from_list(
@@ -1262,7 +1271,8 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
1262
1271
  import sqlite3
1263
1272
 
1264
1273
  if table is None and sql_query is None:
1265
- raise ValueError("Either table or sql_query must be provided")
1274
+ from .exceptions import ValueScenarioError
1275
+ raise ValueScenarioError("Either table or sql_query must be provided")
1266
1276
 
1267
1277
  try:
1268
1278
  with sqlite3.connect(filepath) as conn:
@@ -1328,7 +1338,8 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
1328
1338
  if "/edit" in url:
1329
1339
  doc_id = url.split("/d/")[1].split("/edit")[0]
1330
1340
  else:
1331
- raise ValueError("Invalid Google Doc URL format.")
1341
+ from .exceptions import ValueScenarioError
1342
+ raise ValueScenarioError("Invalid Google Doc URL format.")
1332
1343
 
1333
1344
  export_url = f"https://docs.google.com/document/d/{doc_id}/export?format=docx"
1334
1345
 
@@ -1532,7 +1543,8 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
1532
1543
  print("The Excel file contains multiple sheets:")
1533
1544
  for name in all_sheets.keys():
1534
1545
  print(f"- {name}")
1535
- raise ValueError("Please provide a sheet name to load data from.")
1546
+ from .exceptions import ValueScenarioError
1547
+ raise ValueScenarioError("Please provide a sheet name to load data from.")
1536
1548
  else:
1537
1549
  # If there is only one sheet, use it
1538
1550
  sheet_name = list(all_sheets.keys())[0]
@@ -1587,7 +1599,8 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
1587
1599
  if "/edit" in url:
1588
1600
  sheet_id = url.split("/d/")[1].split("/edit")[0]
1589
1601
  else:
1590
- raise ValueError("Invalid Google Sheet URL format.")
1602
+ from .exceptions import ValueScenarioError
1603
+ raise ValueScenarioError("Invalid Google Sheet URL format.")
1591
1604
 
1592
1605
  export_url = (
1593
1606
  f"https://docs.google.com/spreadsheets/d/{sheet_id}/export?format=xlsx"
@@ -1673,14 +1686,16 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
1673
1686
  file_obj = None
1674
1687
 
1675
1688
  if file_obj is None:
1676
- raise ValueError(f"Could not decode file {source} with any of the attempted encodings. Original error: {last_exception}")
1689
+ from .exceptions import ValueScenarioError
1690
+ raise ValueScenarioError(f"Could not decode file {source} with any of the attempted encodings. Original error: {last_exception}")
1677
1691
 
1678
1692
  reader = csv.reader(file_obj, delimiter=delimiter)
1679
1693
  try:
1680
1694
  header = next(reader)
1681
1695
  observations = [Scenario(dict(zip(header, row))) for row in reader]
1682
1696
  except StopIteration:
1683
- raise ValueError(f"File {source} appears to be empty or has an invalid format")
1697
+ from .exceptions import ValueScenarioError
1698
+ raise ValueScenarioError(f"File {source} appears to be empty or has an invalid format")
1684
1699
 
1685
1700
  finally:
1686
1701
  if file_obj:
@@ -1996,13 +2011,16 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
1996
2011
  import string
1997
2012
 
1998
2013
  if num_options < 2:
1999
- raise ValueError("num_options must be at least 2")
2014
+ from .exceptions import ValueScenarioError
2015
+ raise ValueScenarioError("num_options must be at least 2")
2000
2016
 
2001
2017
  if num_options > len(self):
2002
- raise ValueError(f"num_options ({num_options}) cannot exceed the number of scenarios ({len(self)})")
2018
+ from .exceptions import ValueScenarioError
2019
+ raise ValueScenarioError(f"num_options ({num_options}) cannot exceed the number of scenarios ({len(self)})")
2003
2020
 
2004
2021
  if use_alphabet and num_options > 26:
2005
- raise ValueError("When using alphabet labels, num_options cannot exceed 26 (the number of letters in the English alphabet)")
2022
+ from .exceptions import ValueScenarioError
2023
+ raise ValueScenarioError("When using alphabet labels, num_options cannot exceed 26 (the number of letters in the English alphabet)")
2006
2024
 
2007
2025
  # Convert each scenario to a dictionary
2008
2026
  scenario_dicts = [scenario.to_dict(add_edsl_version=False) for scenario in self]
@@ -1,5 +1,5 @@
1
1
  from typing import Optional
2
- from edsl.utilities.remove_edsl_version import remove_edsl_version
2
+ from ..utilities.remove_edsl_version import remove_edsl_version
3
3
 
4
4
 
5
5
  class CSSRuleMeta(type):
@@ -73,7 +73,7 @@ class CSSRule(metaclass=CSSRuleMeta):
73
73
  d = {"selector": self.selector, "properties": self.properties}
74
74
 
75
75
  if add_esl_version:
76
- from edsl import __version__
76
+ from .. import __version__
77
77
 
78
78
  d["edsl_version"] = __version__
79
79
  d["edsl_class_name"] = self.__class__.__name__
@@ -233,7 +233,7 @@ class SurveyCSS:
233
233
  """
234
234
  d = {"rules": [rule.to_dict() for rule in self.rules.values()]}
235
235
  if add_edsl_version:
236
- from edsl import __version__
236
+ from .. import __version__
237
237
 
238
238
  d["edsl_version"] = __version__
239
239
  d["edsl_class_name"] = self.__class__.__name__
@@ -58,7 +58,8 @@ class Simulator:
58
58
 
59
59
  if num_passes > 100:
60
60
  print("Too many passes.")
61
- raise Exception("Too many passes.")
61
+ from .exceptions import SurveyError
62
+ raise SurveyError("Too many passes.")
62
63
  return self.survey.answers
63
64
 
64
65
  def create_agent(self) -> "Agent":
edsl/tasks/__init__.py CHANGED
@@ -26,9 +26,29 @@ For most users, this module works behind the scenes, but understanding it can
26
26
  be helpful when debugging or optimizing complex EDSL workflows.
27
27
  """
28
28
 
29
- __all__ = ['TaskHistory', 'QuestionTaskCreator', 'TaskCreators', 'TaskStatus', 'TaskStatusDescriptor']
29
+ __all__ = [
30
+ 'TaskHistory',
31
+ 'QuestionTaskCreator',
32
+ 'TaskCreators',
33
+ 'TaskStatus',
34
+ 'TaskStatusDescriptor',
35
+ 'TaskError',
36
+ 'TaskStatusError',
37
+ 'TaskExecutionError',
38
+ 'TaskDependencyError',
39
+ 'TaskResourceError',
40
+ 'TaskHistoryError'
41
+ ]
30
42
 
31
43
  from .task_history import TaskHistory
32
44
  from .question_task_creator import QuestionTaskCreator
33
45
  from .task_creators import TaskCreators
34
- from .task_status_enum import TaskStatus, TaskStatusDescriptor
46
+ from .task_status_enum import TaskStatus, TaskStatusDescriptor
47
+ from .exceptions import (
48
+ TaskError,
49
+ TaskStatusError,
50
+ TaskExecutionError,
51
+ TaskDependencyError,
52
+ TaskResourceError,
53
+ TaskHistoryError
54
+ )
@@ -0,0 +1,72 @@
1
+ """
2
+ This module defines the exception hierarchy for the tasks module.
3
+
4
+ All exceptions related to task creation, execution, and management are defined here.
5
+ These exceptions provide detailed error information for debugging and error reporting.
6
+ """
7
+
8
+ from ..base import BaseException
9
+
10
+
11
+ class TaskError(BaseException):
12
+ """
13
+ Base exception for all tasks-related errors.
14
+
15
+ This is the parent class for all exceptions raised within the tasks module.
16
+ It inherits from BaseException to ensure proper error tracking and reporting.
17
+ """
18
+ pass
19
+
20
+
21
+ class TaskStatusError(TaskError):
22
+ """
23
+ Raised when a task encounters an invalid status transition.
24
+
25
+ This exception is raised when a task attempts to transition to an invalid state
26
+ based on its current state, such as trying to set a completed task to running.
27
+
28
+ Attributes:
29
+ current_status: The current status of the task
30
+ attempted_status: The status that could not be set
31
+ """
32
+ pass
33
+
34
+
35
+ class TaskExecutionError(TaskError):
36
+ """
37
+ Raised when a task encounters an error during execution.
38
+
39
+ This is a general exception for errors that occur while a task is running,
40
+ not specific to dependency resolution or resource allocation.
41
+ """
42
+ pass
43
+
44
+
45
+ class TaskDependencyError(TaskError):
46
+ """
47
+ Raised when there is an issue with task dependencies.
48
+
49
+ This exception is raised for dependency-related issues, such as circular
50
+ dependencies or errors in dependent tasks.
51
+ """
52
+ pass
53
+
54
+
55
+ class TaskResourceError(TaskError):
56
+ """
57
+ Raised when a task cannot acquire necessary resources.
58
+
59
+ This exception is used when a task cannot obtain required resources
60
+ such as tokens or request capacity, beyond normal waiting situations.
61
+ """
62
+ pass
63
+
64
+
65
+ class TaskHistoryError(TaskError):
66
+ """
67
+ Raised for errors related to task history operations.
68
+
69
+ This exception covers issues with recording, accessing, or analyzing
70
+ task execution history and logs.
71
+ """
72
+ pass
@@ -146,7 +146,7 @@ class TaskHistory(RepresentationMixin):
146
146
  "include_traceback": self.include_traceback,
147
147
  }
148
148
  if add_edsl_version:
149
- from edsl import __version__
149
+ from .. import __version__
150
150
 
151
151
  d["edsl_version"] = __version__
152
152
  d["edsl_class_name"] = "TaskHistory"
@@ -302,22 +302,59 @@ class TaskHistory(RepresentationMixin):
302
302
  js = env.joinpath("report.js").read_text()
303
303
  return js
304
304
 
305
+ # @property
306
+ # def exceptions_table(self) -> dict:
307
+ # """Return a dictionary of exceptions organized by type, service, model, and question name."""
308
+ # exceptions_table = {}
309
+ # for interview in self.total_interviews:
310
+ # for question_name, exceptions in interview.exceptions.items():
311
+ # for exception in exceptions:
312
+ # key = (
313
+ # exception.exception.__class__.__name__, # Exception type
314
+ # interview.model._inference_service_, # Service
315
+ # interview.model.model, # Model
316
+ # question_name, # Question name
317
+ # )
318
+ # if key not in exceptions_table:
319
+ # exceptions_table[key] = 0
320
+ # exceptions_table[key] += 1
321
+ # return exceptions_table
322
+
305
323
  @property
306
324
  def exceptions_table(self) -> dict:
307
- """Return a dictionary of exceptions organized by type, service, model, and question name."""
325
+ """Return a dictionary of unique exceptions organized by type, service, model, and question name."""
308
326
  exceptions_table = {}
327
+ seen_exceptions = set()
328
+
309
329
  for interview in self.total_interviews:
310
330
  for question_name, exceptions in interview.exceptions.items():
311
331
  for exception in exceptions:
312
- key = (
332
+ # Create a unique identifier for this exception based on its content
333
+ exception_key = (
313
334
  exception.exception.__class__.__name__, # Exception type
314
- interview.model._inference_service_, # Service
315
- interview.model.model, # Model
316
- question_name, # Question name
335
+ interview.model._inference_service_, # Service
336
+ interview.model.model, # Model
337
+ question_name, # Question name
338
+ exception.name, # Exception name
339
+ str(exception.traceback)[:100] if exception.traceback else "", # Truncated traceback
317
340
  )
318
- if key not in exceptions_table:
319
- exceptions_table[key] = 0
320
- exceptions_table[key] += 1
341
+
342
+ # Only count if we haven't seen this exact exception before
343
+ if exception_key not in seen_exceptions:
344
+ seen_exceptions.add(exception_key)
345
+
346
+ # Add to the summary table
347
+ table_key = (
348
+ exception.exception.__class__.__name__, # Exception type
349
+ interview.model._inference_service_, # Service
350
+ interview.model.model, # Model
351
+ question_name, # Question name
352
+ )
353
+
354
+ if table_key not in exceptions_table:
355
+ exceptions_table[table_key] = 0
356
+ exceptions_table[table_key] += 1
357
+
321
358
  return exceptions_table
322
359
 
323
360
  @property
@@ -406,7 +443,7 @@ class TaskHistory(RepresentationMixin):
406
443
  models_used = set([i.model.model for index, i in self._interviews.items()])
407
444
 
408
445
  from jinja2 import Environment
409
- from edsl.utilities import TemplateLoader
446
+ from ..utilities import TemplateLoader
410
447
 
411
448
  env = Environment(loader=TemplateLoader("edsl", "templates/error_reporting"))
412
449
 
@@ -465,7 +502,7 @@ class TaskHistory(RepresentationMixin):
465
502
  """
466
503
  from IPython.display import display, HTML
467
504
  import os
468
- from edsl.utilities.utilities import is_notebook
505
+ from ..utilities.utilities import is_notebook
469
506
 
470
507
  output = self.generate_html_report(css)
471
508
 
@@ -5,6 +5,39 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Exceptions Report</title>
7
7
  <style>
8
+ /* Global styles */
9
+ :root {
10
+ --primary-color: #3f51b5;
11
+ --secondary-color: #5c6bc0;
12
+ --success-color: #4caf50;
13
+ --error-color: #f44336;
14
+ --warning-color: #ff9800;
15
+ --text-color: #333;
16
+ --light-bg: #f5f7fa;
17
+ --border-color: #e0e0e0;
18
+ --header-bg: #f9f9f9;
19
+ --card-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
20
+ --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
21
+ }
22
+
23
+ * {
24
+ box-sizing: border-box;
25
+ }
26
+
27
+ body {
28
+ font-family: var(--font-family);
29
+ background-color: var(--light-bg);
30
+ color: var(--text-color);
31
+ line-height: 1.6;
32
+ margin: 0;
33
+ padding: 20px;
34
+ }
35
+
36
+ .container {
37
+ max-width: 1200px;
38
+ margin: 0 auto;
39
+ }
40
+
8
41
  {{ css }}
9
42
  </style>
10
43
 
@@ -14,9 +47,9 @@
14
47
 
15
48
  </head>
16
49
  <body>
17
- {% include 'overview.html' %}
18
- {% include 'exceptions_table.html' %}
19
- {% include 'interviews.html' %}
20
- {% include 'performance_plot.html' %}
50
+ <div class="container">
51
+ {% include 'exceptions_table.html' %}
52
+ {% include 'interviews.html' %}
53
+ </div>
21
54
  </body>
22
55
  </html>
@@ -1,34 +1,106 @@
1
+ <div class="summary-section">
2
+ <div class="table-container">
3
+ <h2>Exceptions Report</h2>
4
+ <table class="exceptions-table">
5
+ <thead>
6
+ <tr>
7
+ <th>Exception Type</th>
8
+ <th>Service</th>
9
+ <th>Model</th>
10
+ <th>Question Name</th>
11
+ <th class="count-column">Count</th>
12
+ </tr>
13
+ </thead>
14
+ <tbody>
15
+ {% for (exception_type, service, model, question_name), count in exceptions_table.items() %}
16
+ <tr>
17
+ <td>{{ exception_type }}</td>
18
+ <td>{{ service }}</td>
19
+ <td>{{ model }}</td>
20
+ <td>{{ question_name }}</td>
21
+ <td class="count-cell">{{ count }}</td>
22
+ </tr>
23
+ {% endfor %}
24
+ </tbody>
25
+ </table>
26
+ </div>
27
+
28
+ <p class="note">
29
+ Note: Each unique exception is counted only once. You may encounter repeated exceptions where retries were attempted.
30
+ </p>
31
+ </div>
32
+
1
33
  <style>
2
- th, td {
3
- padding: 0 10px; /* This applies the padding uniformly to all td elements */
4
- }
5
- </style>
6
-
7
- <table border="1">
8
- <thead>
9
- <tr>
10
- <th style="text-align: left">Exception Type</th>
11
- <th style="text-align: left">Service</th>
12
- <th style="text-align: left">Model</th>
13
- <th style="text-align: left">Question Name</th>
14
- <th style="text-align: left">Total</th>
15
- </tr>
16
- </thead>
17
- <tbody>
18
- {% for (exception_type, service, model, question_name), count in exceptions_table.items() %}
19
- <tr>
20
- <td>{{ exception_type }}</td>
21
- <td>{{ service }}</td>
22
- <td>{{ model }}</td>
23
- <td>{{ question_name }}</td>
24
- <td>{{ count }}</td>
25
- </tr>
26
- {% endfor %}
27
- </tbody>
28
- </table>
29
- <p>
30
- Note: You may encounter repeated exceptions where retries were attempted.
31
- </p>
32
- <p>
33
- See details about each exception, including code for reproducing it (click to expand).
34
- </p>
34
+ /* Summary section styles */
35
+ .summary-section {
36
+ background-color: white;
37
+ border-radius: 8px;
38
+ margin-bottom: 24px;
39
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
40
+ overflow: hidden;
41
+ border: 1px solid #e0e0e0;
42
+ padding: 0 0 16px 0;
43
+ }
44
+
45
+ .section-header {
46
+ background-color: #f9f9f9;
47
+ padding: 12px 16px;
48
+ border-bottom: 1px solid #e0e0e0;
49
+ }
50
+
51
+ .section-header h2 {
52
+ margin: 0;
53
+ font-size: 18px;
54
+ font-weight: 500;
55
+ color: #3f51b5;
56
+ }
57
+
58
+ .table-container {
59
+ padding: 16px;
60
+ overflow-x: auto;
61
+ }
62
+
63
+ /* Table styles */
64
+ .exceptions-table {
65
+ width: 100%;
66
+ border-collapse: collapse;
67
+ margin-bottom: 16px;
68
+ }
69
+
70
+ .exceptions-table th {
71
+ background-color: #f5f5f5;
72
+ color: #333;
73
+ font-weight: 500;
74
+ text-align: left;
75
+ padding: 12px;
76
+ border-bottom: 2px solid #e0e0e0;
77
+ }
78
+
79
+ .exceptions-table td {
80
+ padding: 10px 12px;
81
+ border-bottom: 1px solid #e0e0e0;
82
+ color: #333;
83
+ }
84
+
85
+ .exceptions-table tr:hover {
86
+ background-color: #f9f9f9;
87
+ }
88
+
89
+ .count-column {
90
+ width: 80px;
91
+ text-align: center;
92
+ }
93
+
94
+ .count-cell {
95
+ text-align: center;
96
+ font-weight: 500;
97
+ }
98
+
99
+ /* Note styles */
100
+ .note {
101
+ font-size: 14px;
102
+ color: #666;
103
+ margin: 0 16px;
104
+ line-height: 1.5;
105
+ }
106
+ </style>