edsl 0.1.39.dev1__py3-none-any.whl → 0.1.39.dev2__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 (194) hide show
  1. edsl/Base.py +169 -116
  2. edsl/__init__.py +14 -6
  3. edsl/__version__.py +1 -1
  4. edsl/agents/Agent.py +358 -146
  5. edsl/agents/AgentList.py +211 -73
  6. edsl/agents/Invigilator.py +88 -36
  7. edsl/agents/InvigilatorBase.py +59 -70
  8. edsl/agents/PromptConstructor.py +117 -219
  9. edsl/agents/QuestionInstructionPromptBuilder.py +128 -0
  10. edsl/agents/QuestionOptionProcessor.py +172 -0
  11. edsl/agents/QuestionTemplateReplacementsBuilder.py +137 -0
  12. edsl/agents/__init__.py +0 -1
  13. edsl/agents/prompt_helpers.py +3 -3
  14. edsl/config.py +22 -2
  15. edsl/conversation/car_buying.py +2 -1
  16. edsl/coop/CoopFunctionsMixin.py +15 -0
  17. edsl/coop/ExpectedParrotKeyHandler.py +125 -0
  18. edsl/coop/PriceFetcher.py +1 -1
  19. edsl/coop/coop.py +104 -42
  20. edsl/coop/utils.py +14 -14
  21. edsl/data/Cache.py +21 -14
  22. edsl/data/CacheEntry.py +12 -15
  23. edsl/data/CacheHandler.py +33 -12
  24. edsl/data/__init__.py +4 -3
  25. edsl/data_transfer_models.py +2 -1
  26. edsl/enums.py +20 -0
  27. edsl/exceptions/__init__.py +50 -50
  28. edsl/exceptions/agents.py +12 -0
  29. edsl/exceptions/inference_services.py +5 -0
  30. edsl/exceptions/questions.py +24 -6
  31. edsl/exceptions/scenarios.py +7 -0
  32. edsl/inference_services/AnthropicService.py +0 -3
  33. edsl/inference_services/AvailableModelCacheHandler.py +184 -0
  34. edsl/inference_services/AvailableModelFetcher.py +209 -0
  35. edsl/inference_services/AwsBedrock.py +0 -2
  36. edsl/inference_services/AzureAI.py +0 -2
  37. edsl/inference_services/GoogleService.py +2 -11
  38. edsl/inference_services/InferenceServiceABC.py +18 -85
  39. edsl/inference_services/InferenceServicesCollection.py +105 -80
  40. edsl/inference_services/MistralAIService.py +0 -3
  41. edsl/inference_services/OpenAIService.py +1 -4
  42. edsl/inference_services/PerplexityService.py +0 -3
  43. edsl/inference_services/ServiceAvailability.py +135 -0
  44. edsl/inference_services/TestService.py +11 -8
  45. edsl/inference_services/data_structures.py +62 -0
  46. edsl/jobs/AnswerQuestionFunctionConstructor.py +188 -0
  47. edsl/jobs/Answers.py +1 -14
  48. edsl/jobs/FetchInvigilator.py +40 -0
  49. edsl/jobs/InterviewTaskManager.py +98 -0
  50. edsl/jobs/InterviewsConstructor.py +48 -0
  51. edsl/jobs/Jobs.py +102 -243
  52. edsl/jobs/JobsChecks.py +35 -10
  53. edsl/jobs/JobsComponentConstructor.py +189 -0
  54. edsl/jobs/JobsPrompts.py +5 -3
  55. edsl/jobs/JobsRemoteInferenceHandler.py +128 -80
  56. edsl/jobs/JobsRemoteInferenceLogger.py +239 -0
  57. edsl/jobs/RequestTokenEstimator.py +30 -0
  58. edsl/jobs/buckets/BucketCollection.py +44 -3
  59. edsl/jobs/buckets/TokenBucket.py +53 -21
  60. edsl/jobs/buckets/TokenBucketAPI.py +211 -0
  61. edsl/jobs/buckets/TokenBucketClient.py +191 -0
  62. edsl/jobs/decorators.py +35 -0
  63. edsl/jobs/interviews/Interview.py +77 -380
  64. edsl/jobs/jobs_status_enums.py +9 -0
  65. edsl/jobs/loggers/HTMLTableJobLogger.py +304 -0
  66. edsl/jobs/runners/JobsRunnerAsyncio.py +4 -49
  67. edsl/jobs/tasks/QuestionTaskCreator.py +21 -19
  68. edsl/jobs/tasks/TaskHistory.py +14 -15
  69. edsl/jobs/tasks/task_status_enum.py +0 -2
  70. edsl/language_models/ComputeCost.py +63 -0
  71. edsl/language_models/LanguageModel.py +137 -234
  72. edsl/language_models/ModelList.py +11 -13
  73. edsl/language_models/PriceManager.py +127 -0
  74. edsl/language_models/RawResponseHandler.py +106 -0
  75. edsl/language_models/ServiceDataSources.py +0 -0
  76. edsl/language_models/__init__.py +0 -1
  77. edsl/language_models/key_management/KeyLookup.py +63 -0
  78. edsl/language_models/key_management/KeyLookupBuilder.py +273 -0
  79. edsl/language_models/key_management/KeyLookupCollection.py +38 -0
  80. edsl/language_models/key_management/__init__.py +0 -0
  81. edsl/language_models/key_management/models.py +131 -0
  82. edsl/language_models/registry.py +49 -59
  83. edsl/language_models/repair.py +2 -2
  84. edsl/language_models/utilities.py +5 -4
  85. edsl/notebooks/Notebook.py +19 -14
  86. edsl/notebooks/NotebookToLaTeX.py +142 -0
  87. edsl/prompts/Prompt.py +29 -39
  88. edsl/questions/AnswerValidatorMixin.py +47 -2
  89. edsl/questions/ExceptionExplainer.py +77 -0
  90. edsl/questions/HTMLQuestion.py +103 -0
  91. edsl/questions/LoopProcessor.py +149 -0
  92. edsl/questions/QuestionBase.py +37 -192
  93. edsl/questions/QuestionBaseGenMixin.py +52 -48
  94. edsl/questions/QuestionBasePromptsMixin.py +7 -3
  95. edsl/questions/QuestionCheckBox.py +1 -1
  96. edsl/questions/QuestionExtract.py +1 -1
  97. edsl/questions/QuestionFreeText.py +1 -2
  98. edsl/questions/QuestionList.py +3 -5
  99. edsl/questions/QuestionMatrix.py +265 -0
  100. edsl/questions/QuestionMultipleChoice.py +66 -22
  101. edsl/questions/QuestionNumerical.py +1 -3
  102. edsl/questions/QuestionRank.py +6 -16
  103. edsl/questions/ResponseValidatorABC.py +37 -11
  104. edsl/questions/ResponseValidatorFactory.py +28 -0
  105. edsl/questions/SimpleAskMixin.py +4 -3
  106. edsl/questions/__init__.py +1 -0
  107. edsl/questions/derived/QuestionLinearScale.py +6 -3
  108. edsl/questions/derived/QuestionTopK.py +1 -1
  109. edsl/questions/descriptors.py +17 -3
  110. edsl/questions/question_registry.py +1 -1
  111. edsl/questions/templates/matrix/__init__.py +1 -0
  112. edsl/questions/templates/matrix/answering_instructions.jinja +5 -0
  113. edsl/questions/templates/matrix/question_presentation.jinja +20 -0
  114. edsl/results/CSSParameterizer.py +1 -1
  115. edsl/results/Dataset.py +170 -7
  116. edsl/results/DatasetExportMixin.py +224 -302
  117. edsl/results/DatasetTree.py +28 -8
  118. edsl/results/MarkdownToDocx.py +122 -0
  119. edsl/results/MarkdownToPDF.py +111 -0
  120. edsl/results/Result.py +192 -206
  121. edsl/results/Results.py +120 -113
  122. edsl/results/ResultsExportMixin.py +2 -0
  123. edsl/results/Selector.py +23 -13
  124. edsl/results/TableDisplay.py +98 -171
  125. edsl/results/TextEditor.py +50 -0
  126. edsl/results/__init__.py +1 -1
  127. edsl/results/smart_objects.py +96 -0
  128. edsl/results/table_data_class.py +12 -0
  129. edsl/results/table_renderers.py +118 -0
  130. edsl/scenarios/ConstructDownloadLink.py +109 -0
  131. edsl/scenarios/DirectoryScanner.py +96 -0
  132. edsl/scenarios/DocumentChunker.py +102 -0
  133. edsl/scenarios/DocxScenario.py +16 -0
  134. edsl/scenarios/FileStore.py +118 -239
  135. edsl/scenarios/PdfExtractor.py +40 -0
  136. edsl/scenarios/Scenario.py +90 -193
  137. edsl/scenarios/ScenarioHtmlMixin.py +4 -3
  138. edsl/scenarios/ScenarioJoin.py +10 -6
  139. edsl/scenarios/ScenarioList.py +383 -240
  140. edsl/scenarios/ScenarioListExportMixin.py +0 -7
  141. edsl/scenarios/ScenarioListPdfMixin.py +15 -37
  142. edsl/scenarios/ScenarioSelector.py +156 -0
  143. edsl/scenarios/__init__.py +1 -2
  144. edsl/scenarios/file_methods.py +85 -0
  145. edsl/scenarios/handlers/__init__.py +13 -0
  146. edsl/scenarios/handlers/csv.py +38 -0
  147. edsl/scenarios/handlers/docx.py +76 -0
  148. edsl/scenarios/handlers/html.py +37 -0
  149. edsl/scenarios/handlers/json.py +111 -0
  150. edsl/scenarios/handlers/latex.py +5 -0
  151. edsl/scenarios/handlers/md.py +51 -0
  152. edsl/scenarios/handlers/pdf.py +68 -0
  153. edsl/scenarios/handlers/png.py +39 -0
  154. edsl/scenarios/handlers/pptx.py +105 -0
  155. edsl/scenarios/handlers/py.py +294 -0
  156. edsl/scenarios/handlers/sql.py +313 -0
  157. edsl/scenarios/handlers/sqlite.py +149 -0
  158. edsl/scenarios/handlers/txt.py +33 -0
  159. edsl/study/ObjectEntry.py +1 -1
  160. edsl/study/SnapShot.py +1 -1
  161. edsl/study/Study.py +5 -12
  162. edsl/surveys/ConstructDAG.py +92 -0
  163. edsl/surveys/EditSurvey.py +221 -0
  164. edsl/surveys/InstructionHandler.py +100 -0
  165. edsl/surveys/MemoryManagement.py +72 -0
  166. edsl/surveys/Rule.py +5 -4
  167. edsl/surveys/RuleCollection.py +25 -27
  168. edsl/surveys/RuleManager.py +172 -0
  169. edsl/surveys/Simulator.py +75 -0
  170. edsl/surveys/Survey.py +199 -771
  171. edsl/surveys/SurveyCSS.py +20 -8
  172. edsl/surveys/{SurveyFlowVisualizationMixin.py → SurveyFlowVisualization.py} +11 -9
  173. edsl/surveys/SurveyToApp.py +141 -0
  174. edsl/surveys/__init__.py +4 -2
  175. edsl/surveys/descriptors.py +6 -2
  176. edsl/surveys/instructions/ChangeInstruction.py +1 -2
  177. edsl/surveys/instructions/Instruction.py +4 -13
  178. edsl/surveys/instructions/InstructionCollection.py +11 -6
  179. edsl/templates/error_reporting/interview_details.html +1 -1
  180. edsl/templates/error_reporting/report.html +1 -1
  181. edsl/tools/plotting.py +1 -1
  182. edsl/utilities/PrettyList.py +56 -0
  183. edsl/utilities/is_notebook.py +18 -0
  184. edsl/utilities/is_valid_variable_name.py +11 -0
  185. edsl/utilities/remove_edsl_version.py +24 -0
  186. edsl/utilities/utilities.py +35 -23
  187. {edsl-0.1.39.dev1.dist-info → edsl-0.1.39.dev2.dist-info}/METADATA +12 -10
  188. edsl-0.1.39.dev2.dist-info/RECORD +352 -0
  189. edsl/language_models/KeyLookup.py +0 -30
  190. edsl/language_models/unused/ReplicateBase.py +0 -83
  191. edsl/results/ResultsDBMixin.py +0 -238
  192. edsl-0.1.39.dev1.dist-info/RECORD +0 -277
  193. {edsl-0.1.39.dev1.dist-info → edsl-0.1.39.dev2.dist-info}/LICENSE +0 -0
  194. {edsl-0.1.39.dev1.dist-info → edsl-0.1.39.dev2.dist-info}/WHEEL +0 -0
@@ -0,0 +1,304 @@
1
+ import re
2
+ import uuid
3
+ from datetime import datetime
4
+ from IPython.display import display, HTML
5
+ from edsl.jobs.JobsRemoteInferenceLogger import JobLogger
6
+ from edsl.jobs.jobs_status_enums import JobsStatus
7
+
8
+
9
+ class HTMLTableJobLogger(JobLogger):
10
+ def __init__(self, verbose=True, theme="auto", **kwargs):
11
+ super().__init__(verbose=verbose)
12
+ self.display_handle = display(HTML(""), display_id=True)
13
+ self.current_message = None
14
+ self.log_id = str(uuid.uuid4())
15
+ self.is_expanded = True
16
+ self.spinner_chars = ["◐", "◓", "◑", "◒"]
17
+ self.spinner_idx = 0
18
+ self.theme = theme # Can be "auto", "light", or "dark"
19
+
20
+ # Initialize CSS once when the logger is created
21
+ self._init_css()
22
+
23
+ def _init_css(self):
24
+ """Initialize the CSS styles with enhanced theme support"""
25
+ css = """
26
+ <style>
27
+ /* Base theme variables */
28
+ :root {
29
+ --jl-bg-primary: #ffffff;
30
+ --jl-bg-secondary: #f5f5f5;
31
+ --jl-border-color: #e0e0e0;
32
+ --jl-text-primary: #24292e;
33
+ --jl-text-secondary: #586069;
34
+ --jl-link-color: #0366d6;
35
+ --jl-success-color: #28a745;
36
+ --jl-error-color: #d73a49;
37
+ --jl-header-bg: #f1f1f1;
38
+ }
39
+
40
+ /* Dark theme variables */
41
+ .theme-dark {
42
+ --jl-bg-primary: #1e1e1e;
43
+ --jl-bg-secondary: #252526;
44
+ --jl-border-color: #2d2d2d;
45
+ --jl-text-primary: #cccccc;
46
+ --jl-text-secondary: #999999;
47
+ --jl-link-color: #4e94ce;
48
+ --jl-success-color: #89d185;
49
+ --jl-error-color: #f14c4c;
50
+ --jl-header-bg: #333333;
51
+ }
52
+
53
+ /* High contrast theme variables */
54
+ .theme-high-contrast {
55
+ --jl-bg-primary: #000000;
56
+ --jl-bg-secondary: #1a1a1a;
57
+ --jl-border-color: #404040;
58
+ --jl-text-primary: #ffffff;
59
+ --jl-text-secondary: #cccccc;
60
+ --jl-link-color: #66b3ff;
61
+ --jl-success-color: #00ff00;
62
+ --jl-error-color: #ff0000;
63
+ --jl-header-bg: #262626;
64
+ }
65
+
66
+ .job-logger {
67
+ font-family: system-ui, -apple-system, sans-serif;
68
+ max-width: 800px;
69
+ margin: 10px 0;
70
+ color: var(--jl-text-primary);
71
+ box-shadow: 0 1px 3px rgba(0,0,0,0.12);
72
+ border-radius: 4px;
73
+ overflow: hidden;
74
+ }
75
+
76
+ .job-logger-header {
77
+ padding: 12px 16px;
78
+ background: var(--jl-header-bg);
79
+ border: none;
80
+ border-radius: 4px 4px 0 0;
81
+ cursor: pointer;
82
+ color: var(--jl-text-primary);
83
+ user-select: none;
84
+ font-weight: 500;
85
+ letter-spacing: 0.3px;
86
+ display: flex;
87
+ justify-content: space-between;
88
+ align-items: center;
89
+ }
90
+
91
+ .theme-select {
92
+ padding: 4px 8px;
93
+ border-radius: 4px;
94
+ border: 1px solid var(--jl-border-color);
95
+ background: var(--jl-bg-primary);
96
+ color: var(--jl-text-primary);
97
+ font-size: 0.9em;
98
+ }
99
+
100
+ .job-logger-table {
101
+ width: 100%;
102
+ border-collapse: separate;
103
+ border-spacing: 0;
104
+ background: var(--jl-bg-primary);
105
+ border: 1px solid var(--jl-border-color);
106
+ margin-top: -1px;
107
+ }
108
+
109
+ .job-logger-cell {
110
+ padding: 12px 16px;
111
+ border-bottom: 1px solid var(--jl-border-color);
112
+ line-height: 1.4;
113
+ }
114
+
115
+ .job-logger-label {
116
+ font-weight: 500;
117
+ color: var(--jl-text-primary);
118
+ width: 25%;
119
+ background: var(--jl-bg-secondary);
120
+ }
121
+
122
+ .job-logger-value {
123
+ color: var(--jl-text-secondary);
124
+ word-break: break-word;
125
+ }
126
+
127
+ .job-logger-status {
128
+ margin: 0;
129
+ padding: 12px 16px;
130
+ background-color: var(--jl-bg-secondary);
131
+ border: 1px solid var(--jl-border-color);
132
+ border-top: none;
133
+ border-radius: 0 0 4px 4px;
134
+ color: var(--jl-text-primary);
135
+ font-size: 0.95em;
136
+ }
137
+ </style>
138
+
139
+ <script>
140
+ class ThemeManager {
141
+ constructor(logId, initialTheme = 'auto') {
142
+ this.logId = logId;
143
+ this.currentTheme = initialTheme;
144
+ this.darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
145
+ this.init();
146
+ }
147
+
148
+ init() {
149
+ this.setupThemeSwitcher();
150
+ this.updateTheme(this.currentTheme);
151
+
152
+ this.darkModeMediaQuery.addListener(() => {
153
+ if (this.currentTheme === 'auto') {
154
+ this.updateTheme('auto');
155
+ }
156
+ });
157
+ }
158
+
159
+ setupThemeSwitcher() {
160
+ const logger = document.querySelector(`#logger-${this.logId}`);
161
+ if (!logger) return;
162
+
163
+ const switcher = document.createElement('div');
164
+ switcher.className = 'theme-switcher';
165
+ switcher.innerHTML = `
166
+ <select id="theme-select-${this.logId}" class="theme-select">
167
+ <option value="auto">Auto</option>
168
+ <option value="light">Light</option>
169
+ <option value="dark">Dark</option>
170
+ <option value="high-contrast">High Contrast</option>
171
+ </select>
172
+ `;
173
+
174
+ const header = logger.querySelector('.job-logger-header');
175
+ header.appendChild(switcher);
176
+
177
+ const select = switcher.querySelector('select');
178
+ select.value = this.currentTheme;
179
+ select.addEventListener('change', (e) => {
180
+ this.updateTheme(e.target.value);
181
+ });
182
+ }
183
+
184
+ updateTheme(theme) {
185
+ const logger = document.querySelector(`#logger-${this.logId}`);
186
+ if (!logger) return;
187
+
188
+ this.currentTheme = theme;
189
+
190
+ logger.classList.remove('theme-light', 'theme-dark', 'theme-high-contrast');
191
+
192
+ if (theme === 'auto') {
193
+ const isDark = this.darkModeMediaQuery.matches;
194
+ logger.classList.add(isDark ? 'theme-dark' : 'theme-light');
195
+ } else {
196
+ logger.classList.add(`theme-${theme}`);
197
+ }
198
+
199
+ try {
200
+ localStorage.setItem('jobLoggerTheme', theme);
201
+ } catch (e) {
202
+ console.warn('Unable to save theme preference:', e);
203
+ }
204
+ }
205
+ }
206
+
207
+ window.initThemeManager = (logId, initialTheme) => {
208
+ new ThemeManager(logId, initialTheme);
209
+ };
210
+ </script>
211
+ """
212
+
213
+ init_script = f"""
214
+ <script>
215
+ document.addEventListener('DOMContentLoaded', () => {{
216
+ window.initThemeManager('{self.log_id}', '{self.theme}');
217
+ }});
218
+ </script>
219
+ """
220
+
221
+ display(HTML(css + init_script))
222
+
223
+ def _get_table_row(self, key: str, value: str) -> str:
224
+ """Generate a table row with key-value pair"""
225
+ return f"""
226
+ <tr>
227
+ <td class="job-logger-cell job-logger-label">{key}</td>
228
+ <td class="job-logger-cell job-logger-value">{value if value else 'None'}</td>
229
+ </tr>
230
+ """
231
+
232
+ def _linkify(self, text: str) -> str:
233
+ """Convert URLs in text to clickable links"""
234
+ url_pattern = r'(https?://[^\s<>"]+|www\.[^\s<>"]+)'
235
+ return re.sub(
236
+ url_pattern,
237
+ r'<a href="\1" target="_blank" class="job-logger-link">\1</a>',
238
+ text,
239
+ )
240
+
241
+ def _get_spinner(self, status: JobsStatus) -> str:
242
+ """Get the current spinner frame if status is running"""
243
+ if status == JobsStatus.RUNNING:
244
+ spinner = self.spinner_chars[self.spinner_idx]
245
+ self.spinner_idx = (self.spinner_idx + 1) % len(self.spinner_chars)
246
+ return f'<span style="margin-right: 8px;">{spinner}</span>'
247
+ elif status == JobsStatus.COMPLETED:
248
+ return (
249
+ '<span style="margin-right: 8px;" class="job-logger-success">✓</span>'
250
+ )
251
+ elif status == JobsStatus.FAILED:
252
+ return '<span style="margin-right: 8px;" class="job-logger-error">✗</span>'
253
+ return ""
254
+
255
+ def _get_html(self, status: JobsStatus = JobsStatus.RUNNING) -> str:
256
+ """Generate the complete HTML display with theme support"""
257
+ info_rows = ""
258
+ for field, _ in self.jobs_info.__annotations__.items():
259
+ if field != "pretty_names":
260
+ value = getattr(self.jobs_info, field)
261
+ value = self._linkify(str(value)) if value else None
262
+ pretty_name = self.jobs_info.pretty_names.get(
263
+ field, field.replace("_", " ").title()
264
+ )
265
+ info_rows += self._get_table_row(pretty_name, value)
266
+
267
+ message_html = ""
268
+ if self.current_message:
269
+ spinner = self._get_spinner(status)
270
+ message_html = f"""
271
+ <div class="job-logger-status">
272
+ {spinner}<strong>Current Status:</strong> {self._linkify(self.current_message)}
273
+ </div>
274
+ """
275
+
276
+ display_style = "block" if self.is_expanded else "none"
277
+ arrow = "▼" if self.is_expanded else "▶"
278
+
279
+ return f"""
280
+ <!-- #region Remove Inference Info -->
281
+ <div id="logger-{self.log_id}" class="job-logger">
282
+ <div class="job-logger-header">
283
+ <span>
284
+ <span id="arrow-{self.log_id}">{arrow}</span>
285
+ Job Status ({datetime.now().strftime('%Y-%m-%d %H:%M:%S')})
286
+ </span>
287
+ </div>
288
+ <div id="content-{self.log_id}" style="display: {display_style};">
289
+ <table class="job-logger-table">
290
+ {info_rows}
291
+ </table>
292
+ {message_html}
293
+ </div>
294
+ </div>
295
+ <!-- # endregion -->
296
+ """
297
+
298
+ def update(self, message: str, status: JobsStatus = JobsStatus.RUNNING):
299
+ """Update the display with new message and current JobsInfo state"""
300
+ self.current_message = message
301
+ if self.verbose:
302
+ self.display_handle.update(HTML(self._get_html(status)))
303
+ else:
304
+ return None
@@ -37,61 +37,17 @@ class JobsRunnerAsyncio:
37
37
  The Jobs object is a collection of interviews that are to be run.
38
38
  """
39
39
 
40
- MAX_CONCURRENT_DEFAULT = 500
41
-
42
- def __init__(self, jobs: "Jobs"):
40
+ def __init__(self, jobs: "Jobs", bucket_collection: "BucketCollection"):
43
41
  self.jobs = jobs
44
42
  self.interviews: List["Interview"] = jobs.interviews()
45
- self.bucket_collection: "BucketCollection" = jobs.bucket_collection
43
+ self.bucket_collection: "BucketCollection" = bucket_collection
44
+
46
45
  self.total_interviews: List["Interview"] = []
47
46
  self._initialized = threading.Event()
48
47
 
49
48
  from edsl.config import CONFIG
50
49
 
51
50
  self.MAX_CONCURRENT = int(CONFIG.get("EDSL_MAX_CONCURRENT_TASKS"))
52
- # print(f"MAX_CONCURRENT: {self.MAX_CONCURRENT}")
53
-
54
- # async def run_async_generator(
55
- # self,
56
- # cache: Cache,
57
- # n: int = 1,
58
- # stop_on_exception: bool = False,
59
- # sidecar_model: Optional[LanguageModel] = None,
60
- # total_interviews: Optional[List["Interview"]] = None,
61
- # raise_validation_errors: bool = False,
62
- # ) -> AsyncGenerator["Result", None]:
63
- # """Creates the tasks, runs them asynchronously, and returns the results as a Results object.
64
-
65
- # Completed tasks are yielded as they are completed.
66
-
67
- # :param n: how many times to run each interview
68
- # :param stop_on_exception: Whether to stop the interview if an exception is raised
69
- # :param sidecar_model: a language model to use in addition to the interview's model
70
- # :param total_interviews: A list of interviews to run can be provided instead.
71
- # :param raise_validation_errors: Whether to raise validation errors
72
- # """
73
- # tasks = []
74
- # if total_interviews: # was already passed in total interviews
75
- # self.total_interviews = total_interviews
76
- # else:
77
- # self.total_interviews = list(
78
- # self._populate_total_interviews(n=n)
79
- # ) # Populate self.total_interviews before creating tasks
80
- # self._initialized.set() # Signal that we're ready
81
-
82
- # for interview in self.total_interviews:
83
- # interviewing_task = self._build_interview_task(
84
- # interview=interview,
85
- # stop_on_exception=stop_on_exception,
86
- # sidecar_model=sidecar_model,
87
- # raise_validation_errors=raise_validation_errors,
88
- # )
89
- # tasks.append(asyncio.create_task(interviewing_task))
90
-
91
- # for task in asyncio.as_completed(tasks):
92
- # result = await task
93
- # self.jobs_runner_status.add_completed_interview(result)
94
- # yield result
95
51
 
96
52
  async def run_async_generator(
97
53
  self,
@@ -297,6 +253,7 @@ class JobsRunnerAsyncio:
297
253
  generated_tokens=generated_tokens_dict,
298
254
  comments_dict=comments_dict,
299
255
  cache_used_dict=cache_used_dictionary,
256
+ indices=interview.indices,
300
257
  )
301
258
  result.interview_hash = hash(interview)
302
259
 
@@ -351,8 +308,6 @@ class JobsRunnerAsyncio:
351
308
  "EDSL_OPEN_EXCEPTION_REPORT_URL", "must be either True or False"
352
309
  )
353
310
 
354
- # print("open_in_browser", open_in_browser)
355
-
356
311
  filepath = results.task_history.html(
357
312
  cta="Open report to see details.",
358
313
  open_in_browser=open_in_browser,
@@ -1,17 +1,17 @@
1
1
  import asyncio
2
- from typing import Callable, Union, List
2
+ from typing import Callable, Union, List, TYPE_CHECKING
3
3
  from collections import UserList, UserDict
4
4
 
5
- from edsl.jobs.buckets import ModelBuckets
6
- from edsl.exceptions import InterviewErrorPriorTaskCanceled
5
+ from edsl.exceptions.jobs import InterviewErrorPriorTaskCanceled
7
6
 
8
- from edsl.jobs.interviews.InterviewStatusDictionary import InterviewStatusDictionary
9
7
  from edsl.jobs.tasks.task_status_enum import TaskStatus, TaskStatusDescriptor
10
8
  from edsl.jobs.tasks.TaskStatusLog import TaskStatusLog
11
- from edsl.jobs.tokens.InterviewTokenUsage import InterviewTokenUsage
12
9
  from edsl.jobs.tokens.TokenUsage import TokenUsage
13
10
  from edsl.jobs.Answers import Answers
14
- from edsl.questions.QuestionBase import QuestionBase
11
+
12
+ if TYPE_CHECKING:
13
+ from edsl.questions.QuestionBase import QuestionBase
14
+ from edsl.jobs.buckets import ModelBuckets
15
15
 
16
16
 
17
17
  class TokensUsed(UserDict):
@@ -24,7 +24,6 @@ class TokensUsed(UserDict):
24
24
 
25
25
  class QuestionTaskCreator(UserList):
26
26
  """Class to create and manage a single question and its dependencies.
27
- The class is an instance of a UserList of tasks that must be completed before the focal task can be run.
28
27
 
29
28
  It is a UserList with all the tasks that must be completed before the focal task can be run.
30
29
  The focal task is the question that we are interested in answering.
@@ -35,9 +34,9 @@ class QuestionTaskCreator(UserList):
35
34
  def __init__(
36
35
  self,
37
36
  *,
38
- question: QuestionBase,
37
+ question: "QuestionBase",
39
38
  answer_question_func: Callable,
40
- model_buckets: ModelBuckets,
39
+ model_buckets: "ModelBuckets",
41
40
  token_estimator: Union[Callable, None] = None,
42
41
  iteration: int = 0,
43
42
  ):
@@ -51,14 +50,15 @@ class QuestionTaskCreator(UserList):
51
50
 
52
51
  """
53
52
  super().__init__([])
54
- # answer_question_func is the 'interview.answer_question_and_record_task" method
55
53
  self.answer_question_func = answer_question_func
56
54
  self.question = question
57
55
  self.iteration = iteration
58
56
 
59
57
  self.model_buckets = model_buckets
58
+
60
59
  self.requests_bucket = self.model_buckets.requests_bucket
61
60
  self.tokens_bucket = self.model_buckets.tokens_bucket
61
+
62
62
  self.status_log = TaskStatusLog()
63
63
 
64
64
  def fake_token_estimator(question):
@@ -125,11 +125,13 @@ class QuestionTaskCreator(UserList):
125
125
 
126
126
  await self.tokens_bucket.get_tokens(requested_tokens)
127
127
 
128
- if (estimated_wait_time := self.requests_bucket.wait_time(1)) > 0:
128
+ if (estimated_wait_time := self.model_buckets.requests_bucket.wait_time(1)) > 0:
129
129
  self.waiting = True # do we need this?
130
130
  self.task_status = TaskStatus.WAITING_FOR_REQUEST_CAPACITY
131
131
 
132
- await self.requests_bucket.get_tokens(1, cheat_bucket_capacity=True)
132
+ await self.model_buckets.requests_bucket.get_tokens(
133
+ 1, cheat_bucket_capacity=True
134
+ )
133
135
 
134
136
  self.task_status = TaskStatus.API_CALL_IN_PROGRESS
135
137
  try:
@@ -142,22 +144,22 @@ class QuestionTaskCreator(UserList):
142
144
  raise e
143
145
 
144
146
  if results.cache_used:
145
- self.tokens_bucket.add_tokens(requested_tokens)
146
- self.requests_bucket.add_tokens(1)
147
+ self.model_buckets.tokens_bucket.add_tokens(requested_tokens)
148
+ self.model_buckets.requests_bucket.add_tokens(1)
147
149
  self.from_cache = True
148
150
  # Turbo mode means that we don't wait for tokens or requests.
149
- self.tokens_bucket.turbo_mode_on()
150
- self.requests_bucket.turbo_mode_on()
151
+ self.model_buckets.tokens_bucket.turbo_mode_on()
152
+ self.model_buckets.requests_bucket.turbo_mode_on()
151
153
  else:
152
- self.tokens_bucket.turbo_mode_off()
153
- self.requests_bucket.turbo_mode_off()
154
+ self.model_buckets.tokens_bucket.turbo_mode_off()
155
+ self.model_buckets.requests_bucket.turbo_mode_off()
154
156
 
155
157
  return results
156
158
 
157
159
  @classmethod
158
160
  def example(cls):
159
161
  """Return an example instance of the class."""
160
- from edsl import QuestionFreeText
162
+ from edsl.questions.QuestionFreeText import QuestionFreeText
161
163
  from edsl.jobs.buckets.ModelBuckets import ModelBuckets
162
164
 
163
165
  m = ModelBuckets.infinity_bucket()
@@ -1,13 +1,11 @@
1
1
  from typing import List, Optional
2
2
  from io import BytesIO
3
- import webbrowser
4
- import os
5
3
  import base64
6
- from importlib import resources
7
4
  from edsl.jobs.tasks.task_status_enum import TaskStatus
5
+ from edsl.Base import RepresentationMixin
8
6
 
9
7
 
10
- class TaskHistory:
8
+ class TaskHistory(RepresentationMixin):
11
9
  def __init__(
12
10
  self,
13
11
  interviews: List["Interview"],
@@ -121,14 +119,6 @@ class TaskHistory:
121
119
  """Return True if there are any exceptions."""
122
120
  return len(self.unfixed_exceptions) > 0
123
121
 
124
- def _repr_html_(self):
125
- """Return an HTML representation of the TaskHistory."""
126
- d = self.to_dict(add_edsl_version=False)
127
- data = [[k, v] for k, v in d.items()]
128
- from tabulate import tabulate
129
-
130
- return tabulate(data, headers=["keys", "values"], tablefmt="html")
131
-
132
122
  def show_exceptions(self, tracebacks=False):
133
123
  """Print the exceptions."""
134
124
  for index in self.indices:
@@ -240,11 +230,15 @@ class TaskHistory:
240
230
  plt.show()
241
231
 
242
232
  def css(self):
233
+ from importlib import resources
234
+
243
235
  env = resources.files("edsl").joinpath("templates/error_reporting")
244
236
  css = env.joinpath("report.css").read_text()
245
237
  return css
246
238
 
247
239
  def javascript(self):
240
+ from importlib import resources
241
+
248
242
  env = resources.files("edsl").joinpath("templates/error_reporting")
249
243
  js = env.joinpath("report.js").read_text()
250
244
  return js
@@ -281,7 +275,7 @@ class TaskHistory:
281
275
  exceptions_by_question_name = {}
282
276
  for interview in self.total_interviews:
283
277
  for question_name, exceptions in interview.exceptions.items():
284
- question_type = interview.survey.get_question(
278
+ question_type = interview.survey._get_question_by_name(
285
279
  question_name
286
280
  ).question_type
287
281
  if (question_name, question_type) not in exceptions_by_question_name:
@@ -330,8 +324,11 @@ class TaskHistory:
330
324
  }
331
325
  return sorted_exceptions_by_model
332
326
 
333
- def generate_html_report(self, css: Optional[str]):
334
- performance_plot_html = self.plot(num_periods=100, get_embedded_html=True)
327
+ def generate_html_report(self, css: Optional[str], include_plot=False):
328
+ if include_plot:
329
+ performance_plot_html = self.plot(num_periods=100, get_embedded_html=True)
330
+ else:
331
+ performance_plot_html = ""
335
332
 
336
333
  if css is None:
337
334
  css = self.css()
@@ -409,6 +406,8 @@ class TaskHistory:
409
406
  print(f"Exception report saved to {filename}")
410
407
 
411
408
  if open_in_browser:
409
+ import webbrowser
410
+
412
411
  webbrowser.open(f"file://{os.path.abspath(filename)}")
413
412
 
414
413
  if return_link:
@@ -3,8 +3,6 @@ from collections import UserDict
3
3
  import enum
4
4
  import time
5
5
 
6
- # from edsl.jobs.tasks.TaskStatusLogEntry import TaskStatusLogEntry
7
-
8
6
 
9
7
  class TaskStatus(enum.Enum):
10
8
  "These are the possible states a task can be in."
@@ -0,0 +1,63 @@
1
+ from typing import Any, Union
2
+
3
+
4
+ class ComputeCost:
5
+ def __init__(self, language_model: "LanguageModel"):
6
+ self.language_model = language_model
7
+ self._price_lookup = None
8
+
9
+ @property
10
+ def price_lookup(self):
11
+ if self._price_lookup is None:
12
+ from edsl.coop import Coop
13
+
14
+ c = Coop()
15
+ self._price_lookup = c.fetch_prices()
16
+ return self._price_lookup
17
+
18
+ def cost(self, raw_response: dict[str, Any]) -> Union[float, str]:
19
+ """Return the dollar cost of a raw response."""
20
+
21
+ usage = self.get_usage_dict(raw_response)
22
+ from edsl.coop import Coop
23
+
24
+ c = Coop()
25
+ price_lookup = c.fetch_prices()
26
+ key = (self._inference_service_, self.model)
27
+ if key not in price_lookup:
28
+ return f"Could not find price for model {self.model} in the price lookup."
29
+
30
+ relevant_prices = price_lookup[key]
31
+ try:
32
+ input_tokens = int(usage[self.input_token_name])
33
+ output_tokens = int(usage[self.output_token_name])
34
+ except Exception as e:
35
+ return f"Could not fetch tokens from model response: {e}"
36
+
37
+ try:
38
+ inverse_output_price = relevant_prices["output"]["one_usd_buys"]
39
+ inverse_input_price = relevant_prices["input"]["one_usd_buys"]
40
+ except Exception as e:
41
+ if "output" not in relevant_prices:
42
+ return f"Could not fetch prices from {relevant_prices} - {e}; Missing 'output' key."
43
+ if "input" not in relevant_prices:
44
+ return f"Could not fetch prices from {relevant_prices} - {e}; Missing 'input' key."
45
+ return f"Could not fetch prices from {relevant_prices} - {e}"
46
+
47
+ if inverse_input_price == "infinity":
48
+ input_cost = 0
49
+ else:
50
+ try:
51
+ input_cost = input_tokens / float(inverse_input_price)
52
+ except Exception as e:
53
+ return f"Could not compute input price - {e}."
54
+
55
+ if inverse_output_price == "infinity":
56
+ output_cost = 0
57
+ else:
58
+ try:
59
+ output_cost = output_tokens / float(inverse_output_price)
60
+ except Exception as e:
61
+ return f"Could not compute output price - {e}"
62
+
63
+ return input_cost + output_cost