edsl 0.1.38.dev4__py3-none-any.whl → 0.1.39__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 +197 -116
  2. edsl/__init__.py +15 -7
  3. edsl/__version__.py +1 -1
  4. edsl/agents/Agent.py +351 -147
  5. edsl/agents/AgentList.py +211 -73
  6. edsl/agents/Invigilator.py +101 -50
  7. edsl/agents/InvigilatorBase.py +62 -70
  8. edsl/agents/PromptConstructor.py +143 -225
  9. edsl/agents/QuestionInstructionPromptBuilder.py +128 -0
  10. edsl/agents/QuestionTemplateReplacementsBuilder.py +137 -0
  11. edsl/agents/__init__.py +0 -1
  12. edsl/agents/prompt_helpers.py +3 -3
  13. edsl/agents/question_option_processor.py +172 -0
  14. edsl/auto/AutoStudy.py +18 -5
  15. edsl/auto/StageBase.py +53 -40
  16. edsl/auto/StageQuestions.py +2 -1
  17. edsl/auto/utilities.py +0 -6
  18. edsl/config.py +22 -2
  19. edsl/conversation/car_buying.py +2 -1
  20. edsl/coop/CoopFunctionsMixin.py +15 -0
  21. edsl/coop/ExpectedParrotKeyHandler.py +125 -0
  22. edsl/coop/PriceFetcher.py +1 -1
  23. edsl/coop/coop.py +125 -47
  24. edsl/coop/utils.py +14 -14
  25. edsl/data/Cache.py +45 -27
  26. edsl/data/CacheEntry.py +12 -15
  27. edsl/data/CacheHandler.py +31 -12
  28. edsl/data/RemoteCacheSync.py +154 -46
  29. edsl/data/__init__.py +4 -3
  30. edsl/data_transfer_models.py +2 -1
  31. edsl/enums.py +27 -0
  32. edsl/exceptions/__init__.py +50 -50
  33. edsl/exceptions/agents.py +12 -0
  34. edsl/exceptions/inference_services.py +5 -0
  35. edsl/exceptions/questions.py +24 -6
  36. edsl/exceptions/scenarios.py +7 -0
  37. edsl/inference_services/AnthropicService.py +38 -19
  38. edsl/inference_services/AvailableModelCacheHandler.py +184 -0
  39. edsl/inference_services/AvailableModelFetcher.py +215 -0
  40. edsl/inference_services/AwsBedrock.py +0 -2
  41. edsl/inference_services/AzureAI.py +0 -2
  42. edsl/inference_services/GoogleService.py +7 -12
  43. edsl/inference_services/InferenceServiceABC.py +18 -85
  44. edsl/inference_services/InferenceServicesCollection.py +120 -79
  45. edsl/inference_services/MistralAIService.py +0 -3
  46. edsl/inference_services/OpenAIService.py +47 -35
  47. edsl/inference_services/PerplexityService.py +0 -3
  48. edsl/inference_services/ServiceAvailability.py +135 -0
  49. edsl/inference_services/TestService.py +11 -10
  50. edsl/inference_services/TogetherAIService.py +5 -3
  51. edsl/inference_services/data_structures.py +134 -0
  52. edsl/jobs/AnswerQuestionFunctionConstructor.py +223 -0
  53. edsl/jobs/Answers.py +1 -14
  54. edsl/jobs/FetchInvigilator.py +47 -0
  55. edsl/jobs/InterviewTaskManager.py +98 -0
  56. edsl/jobs/InterviewsConstructor.py +50 -0
  57. edsl/jobs/Jobs.py +356 -431
  58. edsl/jobs/JobsChecks.py +35 -10
  59. edsl/jobs/JobsComponentConstructor.py +189 -0
  60. edsl/jobs/JobsPrompts.py +6 -4
  61. edsl/jobs/JobsRemoteInferenceHandler.py +205 -133
  62. edsl/jobs/JobsRemoteInferenceLogger.py +239 -0
  63. edsl/jobs/RequestTokenEstimator.py +30 -0
  64. edsl/jobs/async_interview_runner.py +138 -0
  65. edsl/jobs/buckets/BucketCollection.py +44 -3
  66. edsl/jobs/buckets/TokenBucket.py +53 -21
  67. edsl/jobs/buckets/TokenBucketAPI.py +211 -0
  68. edsl/jobs/buckets/TokenBucketClient.py +191 -0
  69. edsl/jobs/check_survey_scenario_compatibility.py +85 -0
  70. edsl/jobs/data_structures.py +120 -0
  71. edsl/jobs/decorators.py +35 -0
  72. edsl/jobs/interviews/Interview.py +143 -408
  73. edsl/jobs/jobs_status_enums.py +9 -0
  74. edsl/jobs/loggers/HTMLTableJobLogger.py +304 -0
  75. edsl/jobs/results_exceptions_handler.py +98 -0
  76. edsl/jobs/runners/JobsRunnerAsyncio.py +88 -403
  77. edsl/jobs/runners/JobsRunnerStatus.py +133 -165
  78. edsl/jobs/tasks/QuestionTaskCreator.py +21 -19
  79. edsl/jobs/tasks/TaskHistory.py +38 -18
  80. edsl/jobs/tasks/task_status_enum.py +0 -2
  81. edsl/language_models/ComputeCost.py +63 -0
  82. edsl/language_models/LanguageModel.py +194 -236
  83. edsl/language_models/ModelList.py +28 -19
  84. edsl/language_models/PriceManager.py +127 -0
  85. edsl/language_models/RawResponseHandler.py +106 -0
  86. edsl/language_models/ServiceDataSources.py +0 -0
  87. edsl/language_models/__init__.py +1 -2
  88. edsl/language_models/key_management/KeyLookup.py +63 -0
  89. edsl/language_models/key_management/KeyLookupBuilder.py +273 -0
  90. edsl/language_models/key_management/KeyLookupCollection.py +38 -0
  91. edsl/language_models/key_management/__init__.py +0 -0
  92. edsl/language_models/key_management/models.py +131 -0
  93. edsl/language_models/model.py +256 -0
  94. edsl/language_models/repair.py +2 -2
  95. edsl/language_models/utilities.py +5 -4
  96. edsl/notebooks/Notebook.py +19 -14
  97. edsl/notebooks/NotebookToLaTeX.py +142 -0
  98. edsl/prompts/Prompt.py +29 -39
  99. edsl/questions/ExceptionExplainer.py +77 -0
  100. edsl/questions/HTMLQuestion.py +103 -0
  101. edsl/questions/QuestionBase.py +68 -214
  102. edsl/questions/QuestionBasePromptsMixin.py +7 -3
  103. edsl/questions/QuestionBudget.py +1 -1
  104. edsl/questions/QuestionCheckBox.py +3 -3
  105. edsl/questions/QuestionExtract.py +5 -7
  106. edsl/questions/QuestionFreeText.py +2 -3
  107. edsl/questions/QuestionList.py +10 -18
  108. edsl/questions/QuestionMatrix.py +265 -0
  109. edsl/questions/QuestionMultipleChoice.py +67 -23
  110. edsl/questions/QuestionNumerical.py +2 -4
  111. edsl/questions/QuestionRank.py +7 -17
  112. edsl/questions/SimpleAskMixin.py +4 -3
  113. edsl/questions/__init__.py +2 -1
  114. edsl/questions/{AnswerValidatorMixin.py → answer_validator_mixin.py} +47 -2
  115. edsl/questions/data_structures.py +20 -0
  116. edsl/questions/derived/QuestionLinearScale.py +6 -3
  117. edsl/questions/derived/QuestionTopK.py +1 -1
  118. edsl/questions/descriptors.py +17 -3
  119. edsl/questions/loop_processor.py +149 -0
  120. edsl/questions/{QuestionBaseGenMixin.py → question_base_gen_mixin.py} +57 -50
  121. edsl/questions/question_registry.py +1 -1
  122. edsl/questions/{ResponseValidatorABC.py → response_validator_abc.py} +40 -26
  123. edsl/questions/response_validator_factory.py +34 -0
  124. edsl/questions/templates/matrix/__init__.py +1 -0
  125. edsl/questions/templates/matrix/answering_instructions.jinja +5 -0
  126. edsl/questions/templates/matrix/question_presentation.jinja +20 -0
  127. edsl/results/CSSParameterizer.py +1 -1
  128. edsl/results/Dataset.py +170 -7
  129. edsl/results/DatasetExportMixin.py +168 -305
  130. edsl/results/DatasetTree.py +28 -8
  131. edsl/results/MarkdownToDocx.py +122 -0
  132. edsl/results/MarkdownToPDF.py +111 -0
  133. edsl/results/Result.py +298 -206
  134. edsl/results/Results.py +149 -131
  135. edsl/results/ResultsExportMixin.py +2 -0
  136. edsl/results/TableDisplay.py +98 -171
  137. edsl/results/TextEditor.py +50 -0
  138. edsl/results/__init__.py +1 -1
  139. edsl/results/file_exports.py +252 -0
  140. edsl/results/{Selector.py → results_selector.py} +23 -13
  141. edsl/results/smart_objects.py +96 -0
  142. edsl/results/table_data_class.py +12 -0
  143. edsl/results/table_renderers.py +118 -0
  144. edsl/scenarios/ConstructDownloadLink.py +109 -0
  145. edsl/scenarios/DocumentChunker.py +102 -0
  146. edsl/scenarios/DocxScenario.py +16 -0
  147. edsl/scenarios/FileStore.py +150 -239
  148. edsl/scenarios/PdfExtractor.py +40 -0
  149. edsl/scenarios/Scenario.py +90 -193
  150. edsl/scenarios/ScenarioHtmlMixin.py +4 -3
  151. edsl/scenarios/ScenarioList.py +415 -244
  152. edsl/scenarios/ScenarioListExportMixin.py +0 -7
  153. edsl/scenarios/ScenarioListPdfMixin.py +15 -37
  154. edsl/scenarios/__init__.py +1 -2
  155. edsl/scenarios/directory_scanner.py +96 -0
  156. edsl/scenarios/file_methods.py +85 -0
  157. edsl/scenarios/handlers/__init__.py +13 -0
  158. edsl/scenarios/handlers/csv.py +49 -0
  159. edsl/scenarios/handlers/docx.py +76 -0
  160. edsl/scenarios/handlers/html.py +37 -0
  161. edsl/scenarios/handlers/json.py +111 -0
  162. edsl/scenarios/handlers/latex.py +5 -0
  163. edsl/scenarios/handlers/md.py +51 -0
  164. edsl/scenarios/handlers/pdf.py +68 -0
  165. edsl/scenarios/handlers/png.py +39 -0
  166. edsl/scenarios/handlers/pptx.py +105 -0
  167. edsl/scenarios/handlers/py.py +294 -0
  168. edsl/scenarios/handlers/sql.py +313 -0
  169. edsl/scenarios/handlers/sqlite.py +149 -0
  170. edsl/scenarios/handlers/txt.py +33 -0
  171. edsl/scenarios/{ScenarioJoin.py → scenario_join.py} +10 -6
  172. edsl/scenarios/scenario_selector.py +156 -0
  173. edsl/study/ObjectEntry.py +1 -1
  174. edsl/study/SnapShot.py +1 -1
  175. edsl/study/Study.py +5 -12
  176. edsl/surveys/ConstructDAG.py +92 -0
  177. edsl/surveys/EditSurvey.py +221 -0
  178. edsl/surveys/InstructionHandler.py +100 -0
  179. edsl/surveys/MemoryManagement.py +72 -0
  180. edsl/surveys/Rule.py +5 -4
  181. edsl/surveys/RuleCollection.py +25 -27
  182. edsl/surveys/RuleManager.py +172 -0
  183. edsl/surveys/Simulator.py +75 -0
  184. edsl/surveys/Survey.py +270 -791
  185. edsl/surveys/SurveyCSS.py +20 -8
  186. edsl/surveys/{SurveyFlowVisualizationMixin.py → SurveyFlowVisualization.py} +11 -9
  187. edsl/surveys/SurveyToApp.py +141 -0
  188. edsl/surveys/__init__.py +4 -2
  189. edsl/surveys/descriptors.py +6 -2
  190. edsl/surveys/instructions/ChangeInstruction.py +1 -2
  191. edsl/surveys/instructions/Instruction.py +4 -13
  192. edsl/surveys/instructions/InstructionCollection.py +11 -6
  193. edsl/templates/error_reporting/interview_details.html +1 -1
  194. edsl/templates/error_reporting/report.html +1 -1
  195. edsl/tools/plotting.py +1 -1
  196. edsl/utilities/PrettyList.py +56 -0
  197. edsl/utilities/is_notebook.py +18 -0
  198. edsl/utilities/is_valid_variable_name.py +11 -0
  199. edsl/utilities/remove_edsl_version.py +24 -0
  200. edsl/utilities/utilities.py +35 -23
  201. {edsl-0.1.38.dev4.dist-info → edsl-0.1.39.dist-info}/METADATA +12 -10
  202. edsl-0.1.39.dist-info/RECORD +358 -0
  203. {edsl-0.1.38.dev4.dist-info → edsl-0.1.39.dist-info}/WHEEL +1 -1
  204. edsl/language_models/KeyLookup.py +0 -30
  205. edsl/language_models/registry.py +0 -190
  206. edsl/language_models/unused/ReplicateBase.py +0 -83
  207. edsl/results/ResultsDBMixin.py +0 -238
  208. edsl-0.1.38.dev4.dist-info/RECORD +0 -277
  209. /edsl/questions/{RegisterQuestionsMeta.py → register_questions_meta.py} +0 -0
  210. /edsl/results/{ResultsFetchMixin.py → results_fetch_mixin.py} +0 -0
  211. /edsl/results/{ResultsToolsMixin.py → results_tools_mixin.py} +0 -0
  212. {edsl-0.1.38.dev4.dist-info → edsl-0.1.39.dist-info}/LICENSE +0 -0
edsl/results/Result.py CHANGED
@@ -1,81 +1,61 @@
1
1
  # """This module contains the Result class, which captures the result of one interview."""
2
2
  from __future__ import annotations
3
+ import inspect
3
4
  from collections import UserDict
4
- from typing import Any, Type, Callable, Optional
5
- from collections import UserDict
5
+ from typing import Any, Type, Callable, Optional, TYPE_CHECKING, Union
6
6
  from edsl.Base import Base
7
7
  from edsl.utilities.decorators import add_edsl_version, remove_edsl_version
8
8
 
9
+ if TYPE_CHECKING:
10
+ from edsl.agents.Agent import Agent
11
+ from edsl.scenarios.Scenario import Scenario
12
+ from edsl.language_models.LanguageModel import LanguageModel
13
+ from edsl.prompts.Prompt import Prompt
14
+ from edsl.surveys.Survey import Survey
9
15
 
10
- class PromptDict(UserDict):
11
- """A dictionary that is used to store the prompt for a given result."""
12
-
13
- def rich_print(self):
14
- """Display an object as a table."""
15
- from rich.table import Table
16
-
17
- table = Table(title="")
18
- table.add_column("Attribute", style="bold")
19
- table.add_column("Value")
20
16
 
21
- to_display = self
22
- for attr_name, attr_value in to_display.items():
23
- table.add_row(attr_name, repr(attr_value))
17
+ QuestionName = str
18
+ AnswerValue = Any
24
19
 
25
- return table
26
20
 
21
+ class AgentNamer:
22
+ """Maintains a registry of agent names to ensure unique naming."""
27
23
 
28
- def agent_namer_closure():
29
- """Return a function that can be used to name an agent."""
30
- agent_dict = {}
24
+ def __init__(self):
25
+ self._registry = {}
31
26
 
32
- def agent_namer(agent):
33
- """Return a name for an agent. If the agent has been named before, return the same name. Otherwise, return a new name."""
34
- nonlocal agent_dict
35
- agent_count = len(agent_dict)
36
- if id(agent) in agent_dict:
37
- return agent_dict[id(agent)]
38
- else:
39
- agent_dict[id(agent)] = f"Agent_{agent_count}"
40
- return agent_dict[id(agent)]
41
-
42
- return agent_namer
27
+ def get_name(self, agent: "Agent") -> str:
28
+ """Get or create a unique name for an agent."""
29
+ agent_id = id(agent)
30
+ if agent_id not in self._registry:
31
+ self._registry[agent_id] = f"Agent_{len(self._registry)}"
32
+ return self._registry[agent_id]
43
33
 
44
34
 
45
- agent_namer = agent_namer_closure()
35
+ # Global instance for agent naming
36
+ agent_namer = AgentNamer().get_name
46
37
 
47
38
 
48
39
  class Result(Base, UserDict):
49
40
  """
50
41
  This class captures the result of one interview.
51
-
52
- The answer dictionary has the structure:
53
-
54
- >>> import warnings
55
- >>> warnings.simplefilter("ignore", UserWarning)
56
- >>> Result.example().answer == {'how_feeling_yesterday': 'Great', 'how_feeling': 'OK'}
57
- True
58
-
59
- Its main data is an Agent, a Scenario, a Model, an Iteration, and an Answer.
60
- These are stored both in the UserDict and as attributes.
61
-
62
-
63
42
  """
64
43
 
65
44
  def __init__(
66
45
  self,
67
46
  agent: "Agent",
68
47
  scenario: "Scenario",
69
- model: Type["LanguageModel"],
48
+ model: "LanguageModel",
70
49
  iteration: int,
71
- answer: str,
72
- prompt: dict[str, str] = None,
73
- raw_model_response=None,
50
+ answer: dict[QuestionName, AnswerValue],
51
+ prompt: dict[QuestionName, str] = None,
52
+ raw_model_response: Optional[dict] = None,
74
53
  survey: Optional["Survey"] = None,
75
- question_to_attributes: Optional[dict] = None,
54
+ question_to_attributes: Optional[dict[QuestionName, Any]] = None,
76
55
  generated_tokens: Optional[dict] = None,
77
56
  comments_dict: Optional[dict] = None,
78
- cache_used_dict: Optional[dict] = None,
57
+ cache_used_dict: Optional[dict[QuestionName, bool]] = None,
58
+ indices: Optional[dict] = None,
79
59
  ):
80
60
  """Initialize a Result object.
81
61
 
@@ -86,26 +66,17 @@ class Result(Base, UserDict):
86
66
  :param answer: The answer string.
87
67
  :param prompt: A dictionary of prompts.
88
68
  :param raw_model_response: The raw model response.
69
+ :param survey: The Survey object.
70
+ :param question_to_attributes: A dictionary of question attributes.
71
+ :param generated_tokens: A dictionary of generated tokens.
72
+ :param comments_dict: A dictionary of comments.
73
+ :param cache_used_dict: A dictionary of cache usage.
74
+ :param indices: A dictionary of indices.
89
75
 
90
76
  """
91
- if question_to_attributes is not None:
92
- question_to_attributes = question_to_attributes
93
- else:
94
- question_to_attributes = {}
95
-
96
- if survey is not None:
97
- question_to_attributes = {
98
- q.question_name: {
99
- "question_text": q.question_text,
100
- "question_type": q.question_type,
101
- "question_options": (
102
- None
103
- if not hasattr(q, "question_options")
104
- else q.question_options
105
- ),
106
- }
107
- for q in survey.questions
108
- }
77
+ self.question_to_attributes = (
78
+ question_to_attributes or self._create_question_to_attributes(survey)
79
+ )
109
80
 
110
81
  data = {
111
82
  "agent": agent,
@@ -118,81 +89,127 @@ class Result(Base, UserDict):
118
89
  "question_to_attributes": question_to_attributes,
119
90
  "generated_tokens": generated_tokens or {},
120
91
  "comments_dict": comments_dict or {},
92
+ "cache_used_dict": cache_used_dict or {},
121
93
  }
122
94
  super().__init__(**data)
123
- # but also store the data as attributes
124
- self.agent = agent
125
- self.scenario = scenario
126
- self.model = model
127
- self.iteration = iteration
128
- self.answer = answer
129
- self.prompt = prompt or {}
130
- self.raw_model_response = raw_model_response or {}
131
- self.survey = survey
132
- self.question_to_attributes = question_to_attributes
133
- self.generated_tokens = generated_tokens
134
- self.comments_dict = comments_dict or {}
135
- self.cache_used_dict = cache_used_dict or {}
136
-
137
- self._combined_dict = None
138
- self._problem_keys = None
139
-
140
- def _repr_html_(self):
141
- # d = self.to_dict(add_edsl_version=False)
142
- d = self.to_dict(add_edsl_version=False)
143
- data = [[k, v] for k, v in d.items()]
144
- from tabulate import tabulate
145
-
146
- table = str(tabulate(data, headers=["keys", "values"], tablefmt="html"))
147
- return f"<pre>{table}</pre>"
148
-
149
- ###############
150
- # Used in Results
151
- ###############
95
+ self.indices = indices
96
+ self._sub_dicts = self._construct_sub_dicts()
97
+ (
98
+ self._combined_dict,
99
+ self._problem_keys,
100
+ ) = self._compute_combined_dict_and_problem_keys()
101
+
102
+ @staticmethod
103
+ def _create_question_to_attributes(survey):
104
+ """Create a dictionary of question attributes."""
105
+ if survey is None:
106
+ return {}
107
+ return {
108
+ q.question_name: {
109
+ "question_text": q.question_text,
110
+ "question_type": q.question_type,
111
+ "question_options": (
112
+ None if not hasattr(q, "question_options") else q.question_options
113
+ ),
114
+ }
115
+ for q in survey.questions
116
+ }
117
+
152
118
  @property
153
- def sub_dicts(self) -> dict[str, dict]:
154
- """Return a dictionary where keys are strings for each of the main class attributes/objects."""
155
- if self.agent.name is None:
156
- agent_name = agent_namer(self.agent)
119
+ def agent(self) -> "Agent":
120
+ """Return the Agent object."""
121
+ return self.data["agent"]
122
+
123
+ @property
124
+ def scenario(self) -> "Scenario":
125
+ """Return the Scenario object."""
126
+ return self.data["scenario"]
127
+
128
+ @property
129
+ def model(self) -> "LanguageModel":
130
+ """Return the LanguageModel object."""
131
+ return self.data["model"]
132
+
133
+ @property
134
+ def answer(self) -> dict[QuestionName, AnswerValue]:
135
+ """Return the answers."""
136
+ return self.data["answer"]
137
+
138
+ @staticmethod
139
+ def _create_agent_sub_dict(agent) -> dict:
140
+ """Create a dictionary of agent details"""
141
+ if agent.name is None:
142
+ agent_name = agent_namer(agent)
157
143
  else:
158
- agent_name = self.agent.name
159
-
160
- # comments_dict = {k: v for k, v in self.answer.items() if k.endswith("_comment")}
161
- question_text_dict = {}
162
- question_options_dict = {}
163
- question_type_dict = {}
164
- for key, _ in self.answer.items():
165
- if key in self.question_to_attributes:
166
- # You might be tempted to just use the naked key
167
- # but this is a bad idea because it pollutes the namespace
168
- question_text_dict[
169
- key + "_question_text"
170
- ] = self.question_to_attributes[key]["question_text"]
171
- question_options_dict[
172
- key + "_question_options"
173
- ] = self.question_to_attributes[key]["question_options"]
174
- question_type_dict[
175
- key + "_question_type"
176
- ] = self.question_to_attributes[key]["question_type"]
144
+ agent_name = agent.name
177
145
 
178
146
  return {
179
- "agent": self.agent.traits
147
+ "agent": agent.traits
180
148
  | {"agent_name": agent_name}
181
- | {"agent_instruction": self.agent.instruction},
182
- "scenario": self.scenario,
183
- "model": self.model.parameters | {"model": self.model.model},
184
- "answer": self.answer,
185
- "prompt": self.prompt,
186
- "raw_model_response": self.raw_model_response,
187
- "iteration": {"iteration": self.iteration},
188
- "question_text": question_text_dict,
189
- "question_options": question_options_dict,
190
- "question_type": question_type_dict,
191
- "comment": self.comments_dict,
192
- "generated_tokens": self.generated_tokens,
149
+ | {"agent_instruction": agent.instruction},
150
+ }
151
+
152
+ @staticmethod
153
+ def _create_model_sub_dict(model) -> dict:
154
+ return {
155
+ "model": model.parameters | {"model": model.model},
156
+ }
157
+
158
+ @staticmethod
159
+ def _iteration_sub_dict(iteration) -> dict:
160
+ return {
161
+ "iteration": {"iteration": iteration},
162
+ }
163
+
164
+ def _construct_sub_dicts(self) -> dict[str, dict]:
165
+ """Construct a dictionary of sub-dictionaries for the Result object."""
166
+ sub_dicts_needing_new_keys = {
167
+ "question_text": {},
168
+ "question_options": {},
169
+ "question_type": {},
170
+ }
171
+
172
+ for question_name in self.data["answer"]:
173
+ if question_name in self.question_to_attributes:
174
+ for dictionary_name in sub_dicts_needing_new_keys:
175
+ new_key = question_name + "_" + dictionary_name
176
+ sub_dicts_needing_new_keys[dictionary_name][new_key] = (
177
+ self.question_to_attributes[question_name][dictionary_name]
178
+ )
179
+
180
+ new_cache_dict = {
181
+ f"{k}_cache_used": v for k, v in self.data["cache_used_dict"].items()
193
182
  }
194
183
 
195
- def check_expression(self, expression) -> None:
184
+ d = {
185
+ **self._create_agent_sub_dict(self.data["agent"]),
186
+ **self._create_model_sub_dict(self.data["model"]),
187
+ **self._iteration_sub_dict(self.data["iteration"]),
188
+ "scenario": self.data["scenario"],
189
+ "answer": self.data["answer"],
190
+ "prompt": self.data["prompt"],
191
+ "comment": self.data["comments_dict"],
192
+ "generated_tokens": self.data["generated_tokens"],
193
+ "raw_model_response": self.data["raw_model_response"],
194
+ "question_text": sub_dicts_needing_new_keys["question_text"],
195
+ "question_options": sub_dicts_needing_new_keys["question_options"],
196
+ "question_type": sub_dicts_needing_new_keys["question_type"],
197
+ "cache_used": new_cache_dict,
198
+ }
199
+ if hasattr(self, "indices") and self.indices is not None:
200
+ d["agent"].update({"agent_index": self.indices["agent"]})
201
+ d["scenario"].update({"scenario_index": self.indices["scenario"]})
202
+ d["model"].update({"model_index": self.indices["model"]})
203
+ return d
204
+
205
+ @property
206
+ def sub_dicts(self) -> dict[str, dict]:
207
+ """Return a dictionary where keys are strings for each of the main class attributes/objects."""
208
+ if self._sub_dicts is None:
209
+ self._sub_dicts = self._construct_sub_dicts()
210
+ return self._sub_dicts
211
+
212
+ def check_expression(self, expression: str) -> None:
196
213
  for key in self.problem_keys:
197
214
  if key in expression and not key + "." in expression:
198
215
  raise ValueError(
@@ -205,11 +222,13 @@ class Result(Base, UserDict):
205
222
  raise NotImplementedError
206
223
 
207
224
  @property
208
- def problem_keys(self):
225
+ def problem_keys(self) -> list[str]:
209
226
  """Return a list of keys that are problematic."""
210
227
  return self._problem_keys
211
228
 
212
- def _compute_combined_dict_and_problem_keys(self) -> None:
229
+ def _compute_combined_dict_and_problem_keys(
230
+ self,
231
+ ) -> tuple[dict[str, Any], list[str]]:
213
232
  combined = {}
214
233
  problem_keys = []
215
234
  for key, sub_dict in self.sub_dicts.items():
@@ -222,8 +241,7 @@ class Result(Base, UserDict):
222
241
  combined.update({key: sub_dict})
223
242
  # I *think* this allows us to do do things like "answer.how_feelling" i.e., that the evaluator can use
224
243
  # dot notation to access the subdicts.
225
- self._combined_dict = combined
226
- self._problem_keys = problem_keys
244
+ return combined, problem_keys
227
245
 
228
246
  @property
229
247
  def combined_dict(self) -> dict[str, Any]:
@@ -234,11 +252,14 @@ class Result(Base, UserDict):
234
252
  'OK'
235
253
  """
236
254
  if self._combined_dict is None or self._problem_keys is None:
237
- self._compute_combined_dict_and_problem_keys()
255
+ (
256
+ self._combined_dict,
257
+ self._problem_keys,
258
+ ) = self._compute_combined_dict_and_problem_keys()
238
259
  return self._combined_dict
239
260
 
240
261
  @property
241
- def problem_keys(self):
262
+ def problem_keys(self) -> list[str]:
242
263
  """Return a list of keys that are problematic."""
243
264
  if self._combined_dict is None or self._problem_keys is None:
244
265
  self._compute_combined_dict_and_problem_keys()
@@ -278,7 +299,6 @@ class Result(Base, UserDict):
278
299
  )
279
300
  problem_keys.append((key, data_type))
280
301
  key = f"{key}_{data_type}"
281
- # raise ValueError(f"Key '{key}' is already in the dictionary")
282
302
  d[key] = data_type
283
303
 
284
304
  for key, data_type in problem_keys:
@@ -287,37 +307,16 @@ class Result(Base, UserDict):
287
307
  ].pop(key)
288
308
  return d
289
309
 
290
- def rows(self, index) -> tuple[int, str, str, str]:
291
- """Return a generator of rows for the Result object."""
292
- for data_type, subdict in self.sub_dicts.items():
293
- for key, value in subdict.items():
294
- yield (index, data_type, key, str(value))
295
-
296
- def leaves(self):
297
- leaves = []
298
- for question_name, answer in self.answer.items():
299
- if not question_name.endswith("_comment"):
300
- leaves.append(
301
- {
302
- "question": f"({question_name}): "
303
- + str(
304
- self.question_to_attributes[question_name]["question_text"]
305
- ),
306
- "answer": answer,
307
- "comment": self.answer.get(question_name + "_comment", ""),
308
- "scenario": repr(self.scenario),
309
- "agent": repr(self.agent),
310
- "model": repr(self.model),
311
- "iteration": self.iteration,
312
- }
313
- )
314
- return leaves
315
-
316
- ###############
317
- # Useful
318
- ###############
319
310
  def copy(self) -> Result:
320
- """Return a copy of the Result object."""
311
+ """Return a copy of the Result object.
312
+
313
+ >>> r = Result.example()
314
+ >>> r2 = r.copy()
315
+ >>> r == r2
316
+ True
317
+ >>> id(r) == id(r2)
318
+ False
319
+ """
321
320
  return Result.from_dict(self.to_dict())
322
321
 
323
322
  def __eq__(self, other) -> bool:
@@ -328,17 +327,16 @@ class Result(Base, UserDict):
328
327
  True
329
328
 
330
329
  """
331
- return self.to_dict() == other.to_dict()
330
+ return hash(self) == hash(other)
332
331
 
333
- ###############
334
- # Serialization
335
- ###############
336
- def to_dict(self, add_edsl_version=True) -> dict[str, Any]:
332
+ def to_dict(
333
+ self, add_edsl_version: bool = True, include_cache_info: bool = False
334
+ ) -> dict[str, Any]:
337
335
  """Return a dictionary representation of the Result object.
338
336
 
339
337
  >>> r = Result.example()
340
338
  >>> r.to_dict()['scenario']
341
- {'period': 'morning', 'edsl_version': '...', 'edsl_class_name': 'Scenario'}
339
+ {'period': 'morning', 'scenario_index': 0, 'edsl_version': '...', 'edsl_class_name': 'Scenario'}
342
340
  """
343
341
 
344
342
  def convert_value(value, add_edsl_version=True):
@@ -366,21 +364,26 @@ class Result(Base, UserDict):
366
364
  d["edsl_version"] = __version__
367
365
  d["edsl_class_name"] = "Result"
368
366
 
367
+ if include_cache_info:
368
+ d["cache_used_dict"] = self.data["cache_used_dict"]
369
+ else:
370
+ d.pop("cache_used_dict", None)
371
+
369
372
  return d
370
373
 
371
374
  def __hash__(self):
372
375
  """Return a hash of the Result object."""
373
376
  from edsl.utilities.utilities import dict_hash
374
377
 
375
- return dict_hash(self.to_dict(add_edsl_version=False))
378
+ return dict_hash(self.to_dict(add_edsl_version=False, include_cache_info=False))
376
379
 
377
380
  @classmethod
378
381
  @remove_edsl_version
379
382
  def from_dict(self, json_dict: dict) -> Result:
380
383
  """Return a Result object from a dictionary representation."""
381
384
 
382
- from edsl import Agent
383
- from edsl import Scenario
385
+ from edsl.agents.Agent import Agent
386
+ from edsl.scenarios.Scenario import Scenario
384
387
  from edsl.language_models.LanguageModel import LanguageModel
385
388
  from edsl.prompts.Prompt import Prompt
386
389
 
@@ -402,51 +405,34 @@ class Result(Base, UserDict):
402
405
  question_to_attributes=json_dict.get("question_to_attributes", None),
403
406
  generated_tokens=json_dict.get("generated_tokens", {}),
404
407
  comments_dict=json_dict.get("comments_dict", {}),
408
+ cache_used_dict=json_dict.get("cache_used_dict", {}),
405
409
  )
406
410
  return result
407
411
 
408
- def rich_print(self) -> None:
409
- """Display an object as a table."""
410
- # from edsl.utilities import print_dict_with_rich
411
- from rich import print
412
- from rich.table import Table
413
-
414
- table = Table(title="Result")
415
- table.add_column("Attribute", style="bold")
416
- table.add_column("Value")
417
-
418
- to_display = self.__dict__.copy()
419
- data = to_display.pop("data", None)
420
- for attr_name, attr_value in to_display.items():
421
- if hasattr(attr_value, "rich_print"):
422
- table.add_row(attr_name, attr_value.rich_print())
423
- elif isinstance(attr_value, dict):
424
- a = PromptDict(attr_value)
425
- table.add_row(attr_name, a.rich_print())
426
- else:
427
- table.add_row(attr_name, repr(attr_value))
428
- return table
429
-
430
412
  def __repr__(self):
431
413
  """Return a string representation of the Result object."""
432
- return f"Result(agent={repr(self.agent)}, scenario={repr(self.scenario)}, model={repr(self.model)}, iteration={self.iteration}, answer={repr(self.answer)}, prompt={repr(self.prompt)})"
414
+ params = ", ".join(f"{key}={repr(value)}" for key, value in self.data.items())
415
+ return f"{self.__class__.__name__}({params})"
433
416
 
434
417
  @classmethod
435
418
  def example(cls):
436
- """Return an example Result object."""
419
+ """Return an example Result object.
420
+
421
+ >>> Result.example()
422
+ Result(...)
423
+
424
+ """
437
425
  from edsl.results.Results import Results
438
426
 
439
427
  return Results.example()[0]
440
428
 
441
- def score(self, scoring_function: Callable) -> Any:
429
+ def score(self, scoring_function: Callable) -> Union[int, float]:
442
430
  """Score the result using a passed-in scoring function.
443
431
 
444
432
  >>> def f(status): return 1 if status == 'Joyful' else 0
445
433
  >>> Result.example().score(f)
446
434
  1
447
435
  """
448
- import inspect
449
-
450
436
  signature = inspect.signature(scoring_function)
451
437
  params = {}
452
438
  for k, v in signature.parameters.items():
@@ -458,6 +444,112 @@ class Result(Base, UserDict):
458
444
  raise ValueError(f"Parameter {k} not found in Result object")
459
445
  return scoring_function(**params)
460
446
 
447
+ @classmethod
448
+ def from_interview(
449
+ cls, interview, extracted_answers, model_response_objects
450
+ ) -> Result:
451
+ """Return a Result object from an interview dictionary."""
452
+
453
+ def get_question_results(
454
+ model_response_objects,
455
+ ) -> dict[str, "EDSLResultObjectInput"]:
456
+ """Maps the question name to the EDSLResultObjectInput."""
457
+ question_results = {}
458
+ for result in model_response_objects:
459
+ question_results[result.question_name] = result
460
+ return question_results
461
+
462
+ def get_generated_tokens_dict(answer_key_names) -> dict[str, str]:
463
+ generated_tokens_dict = {
464
+ k + "_generated_tokens": question_results[k].generated_tokens
465
+ for k in answer_key_names
466
+ }
467
+ return generated_tokens_dict
468
+
469
+ def get_comments_dict(answer_key_names) -> dict[str, str]:
470
+ comments_dict = {
471
+ k + "_comment": question_results[k].comment for k in answer_key_names
472
+ }
473
+ return comments_dict
474
+
475
+ def get_question_name_to_prompts(
476
+ model_response_objects,
477
+ ) -> dict[str, dict[str, str]]:
478
+ question_name_to_prompts = dict({})
479
+ for result in model_response_objects:
480
+ question_name = result.question_name
481
+ question_name_to_prompts[question_name] = {
482
+ "user_prompt": result.prompts["user_prompt"],
483
+ "system_prompt": result.prompts["system_prompt"],
484
+ }
485
+ return question_name_to_prompts
486
+
487
+ def get_prompt_dictionary(answer_key_names, question_name_to_prompts):
488
+ prompt_dictionary = {}
489
+ for answer_key_name in answer_key_names:
490
+ prompt_dictionary[answer_key_name + "_user_prompt"] = (
491
+ question_name_to_prompts[answer_key_name]["user_prompt"]
492
+ )
493
+ prompt_dictionary[answer_key_name + "_system_prompt"] = (
494
+ question_name_to_prompts[answer_key_name]["system_prompt"]
495
+ )
496
+ return prompt_dictionary
497
+
498
+ def get_raw_model_results_and_cache_used_dictionary(model_response_objects):
499
+ raw_model_results_dictionary = {}
500
+ cache_used_dictionary = {}
501
+ for result in model_response_objects:
502
+ question_name = result.question_name
503
+ raw_model_results_dictionary[question_name + "_raw_model_response"] = (
504
+ result.raw_model_response
505
+ )
506
+ raw_model_results_dictionary[question_name + "_cost"] = result.cost
507
+ one_use_buys = (
508
+ "NA"
509
+ if isinstance(result.cost, str)
510
+ or result.cost == 0
511
+ or result.cost is None
512
+ else 1.0 / result.cost
513
+ )
514
+ raw_model_results_dictionary[question_name + "_one_usd_buys"] = (
515
+ one_use_buys
516
+ )
517
+ cache_used_dictionary[question_name] = result.cache_used
518
+
519
+ return raw_model_results_dictionary, cache_used_dictionary
520
+
521
+ question_results = get_question_results(model_response_objects)
522
+ answer_key_names = list(question_results.keys())
523
+ generated_tokens_dict = get_generated_tokens_dict(answer_key_names)
524
+ comments_dict = get_comments_dict(answer_key_names)
525
+ answer_dict = {k: extracted_answers[k] for k in answer_key_names}
526
+
527
+ question_name_to_prompts = get_question_name_to_prompts(model_response_objects)
528
+ prompt_dictionary = get_prompt_dictionary(
529
+ answer_key_names, question_name_to_prompts
530
+ )
531
+ raw_model_results_dictionary, cache_used_dictionary = (
532
+ get_raw_model_results_and_cache_used_dictionary(model_response_objects)
533
+ )
534
+
535
+ result = cls(
536
+ agent=interview.agent,
537
+ scenario=interview.scenario,
538
+ model=interview.model,
539
+ iteration=interview.iteration,
540
+ # Computed objects
541
+ answer=answer_dict,
542
+ prompt=prompt_dictionary,
543
+ raw_model_response=raw_model_results_dictionary,
544
+ survey=interview.survey,
545
+ generated_tokens=generated_tokens_dict,
546
+ comments_dict=comments_dict,
547
+ cache_used_dict=cache_used_dictionary,
548
+ indices=interview.indices,
549
+ )
550
+ result.interview_hash = interview.initial_hash
551
+ return result
552
+
461
553
 
462
554
  if __name__ == "__main__":
463
555
  import doctest