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.
- edsl/Base.py +169 -116
- edsl/__init__.py +14 -6
- edsl/__version__.py +1 -1
- edsl/agents/Agent.py +358 -146
- edsl/agents/AgentList.py +211 -73
- edsl/agents/Invigilator.py +88 -36
- edsl/agents/InvigilatorBase.py +59 -70
- edsl/agents/PromptConstructor.py +117 -219
- edsl/agents/QuestionInstructionPromptBuilder.py +128 -0
- edsl/agents/QuestionOptionProcessor.py +172 -0
- edsl/agents/QuestionTemplateReplacementsBuilder.py +137 -0
- edsl/agents/__init__.py +0 -1
- edsl/agents/prompt_helpers.py +3 -3
- edsl/config.py +22 -2
- edsl/conversation/car_buying.py +2 -1
- edsl/coop/CoopFunctionsMixin.py +15 -0
- edsl/coop/ExpectedParrotKeyHandler.py +125 -0
- edsl/coop/PriceFetcher.py +1 -1
- edsl/coop/coop.py +104 -42
- edsl/coop/utils.py +14 -14
- edsl/data/Cache.py +21 -14
- edsl/data/CacheEntry.py +12 -15
- edsl/data/CacheHandler.py +33 -12
- edsl/data/__init__.py +4 -3
- edsl/data_transfer_models.py +2 -1
- edsl/enums.py +20 -0
- edsl/exceptions/__init__.py +50 -50
- edsl/exceptions/agents.py +12 -0
- edsl/exceptions/inference_services.py +5 -0
- edsl/exceptions/questions.py +24 -6
- edsl/exceptions/scenarios.py +7 -0
- edsl/inference_services/AnthropicService.py +0 -3
- edsl/inference_services/AvailableModelCacheHandler.py +184 -0
- edsl/inference_services/AvailableModelFetcher.py +209 -0
- edsl/inference_services/AwsBedrock.py +0 -2
- edsl/inference_services/AzureAI.py +0 -2
- edsl/inference_services/GoogleService.py +2 -11
- edsl/inference_services/InferenceServiceABC.py +18 -85
- edsl/inference_services/InferenceServicesCollection.py +105 -80
- edsl/inference_services/MistralAIService.py +0 -3
- edsl/inference_services/OpenAIService.py +1 -4
- edsl/inference_services/PerplexityService.py +0 -3
- edsl/inference_services/ServiceAvailability.py +135 -0
- edsl/inference_services/TestService.py +11 -8
- edsl/inference_services/data_structures.py +62 -0
- edsl/jobs/AnswerQuestionFunctionConstructor.py +188 -0
- edsl/jobs/Answers.py +1 -14
- edsl/jobs/FetchInvigilator.py +40 -0
- edsl/jobs/InterviewTaskManager.py +98 -0
- edsl/jobs/InterviewsConstructor.py +48 -0
- edsl/jobs/Jobs.py +102 -243
- edsl/jobs/JobsChecks.py +35 -10
- edsl/jobs/JobsComponentConstructor.py +189 -0
- edsl/jobs/JobsPrompts.py +5 -3
- edsl/jobs/JobsRemoteInferenceHandler.py +128 -80
- edsl/jobs/JobsRemoteInferenceLogger.py +239 -0
- edsl/jobs/RequestTokenEstimator.py +30 -0
- edsl/jobs/buckets/BucketCollection.py +44 -3
- edsl/jobs/buckets/TokenBucket.py +53 -21
- edsl/jobs/buckets/TokenBucketAPI.py +211 -0
- edsl/jobs/buckets/TokenBucketClient.py +191 -0
- edsl/jobs/decorators.py +35 -0
- edsl/jobs/interviews/Interview.py +77 -380
- edsl/jobs/jobs_status_enums.py +9 -0
- edsl/jobs/loggers/HTMLTableJobLogger.py +304 -0
- edsl/jobs/runners/JobsRunnerAsyncio.py +4 -49
- edsl/jobs/tasks/QuestionTaskCreator.py +21 -19
- edsl/jobs/tasks/TaskHistory.py +14 -15
- edsl/jobs/tasks/task_status_enum.py +0 -2
- edsl/language_models/ComputeCost.py +63 -0
- edsl/language_models/LanguageModel.py +137 -234
- edsl/language_models/ModelList.py +11 -13
- edsl/language_models/PriceManager.py +127 -0
- edsl/language_models/RawResponseHandler.py +106 -0
- edsl/language_models/ServiceDataSources.py +0 -0
- edsl/language_models/__init__.py +0 -1
- edsl/language_models/key_management/KeyLookup.py +63 -0
- edsl/language_models/key_management/KeyLookupBuilder.py +273 -0
- edsl/language_models/key_management/KeyLookupCollection.py +38 -0
- edsl/language_models/key_management/__init__.py +0 -0
- edsl/language_models/key_management/models.py +131 -0
- edsl/language_models/registry.py +49 -59
- edsl/language_models/repair.py +2 -2
- edsl/language_models/utilities.py +5 -4
- edsl/notebooks/Notebook.py +19 -14
- edsl/notebooks/NotebookToLaTeX.py +142 -0
- edsl/prompts/Prompt.py +29 -39
- edsl/questions/AnswerValidatorMixin.py +47 -2
- edsl/questions/ExceptionExplainer.py +77 -0
- edsl/questions/HTMLQuestion.py +103 -0
- edsl/questions/LoopProcessor.py +149 -0
- edsl/questions/QuestionBase.py +37 -192
- edsl/questions/QuestionBaseGenMixin.py +52 -48
- edsl/questions/QuestionBasePromptsMixin.py +7 -3
- edsl/questions/QuestionCheckBox.py +1 -1
- edsl/questions/QuestionExtract.py +1 -1
- edsl/questions/QuestionFreeText.py +1 -2
- edsl/questions/QuestionList.py +3 -5
- edsl/questions/QuestionMatrix.py +265 -0
- edsl/questions/QuestionMultipleChoice.py +66 -22
- edsl/questions/QuestionNumerical.py +1 -3
- edsl/questions/QuestionRank.py +6 -16
- edsl/questions/ResponseValidatorABC.py +37 -11
- edsl/questions/ResponseValidatorFactory.py +28 -0
- edsl/questions/SimpleAskMixin.py +4 -3
- edsl/questions/__init__.py +1 -0
- edsl/questions/derived/QuestionLinearScale.py +6 -3
- edsl/questions/derived/QuestionTopK.py +1 -1
- edsl/questions/descriptors.py +17 -3
- edsl/questions/question_registry.py +1 -1
- edsl/questions/templates/matrix/__init__.py +1 -0
- edsl/questions/templates/matrix/answering_instructions.jinja +5 -0
- edsl/questions/templates/matrix/question_presentation.jinja +20 -0
- edsl/results/CSSParameterizer.py +1 -1
- edsl/results/Dataset.py +170 -7
- edsl/results/DatasetExportMixin.py +224 -302
- edsl/results/DatasetTree.py +28 -8
- edsl/results/MarkdownToDocx.py +122 -0
- edsl/results/MarkdownToPDF.py +111 -0
- edsl/results/Result.py +192 -206
- edsl/results/Results.py +120 -113
- edsl/results/ResultsExportMixin.py +2 -0
- edsl/results/Selector.py +23 -13
- edsl/results/TableDisplay.py +98 -171
- edsl/results/TextEditor.py +50 -0
- edsl/results/__init__.py +1 -1
- edsl/results/smart_objects.py +96 -0
- edsl/results/table_data_class.py +12 -0
- edsl/results/table_renderers.py +118 -0
- edsl/scenarios/ConstructDownloadLink.py +109 -0
- edsl/scenarios/DirectoryScanner.py +96 -0
- edsl/scenarios/DocumentChunker.py +102 -0
- edsl/scenarios/DocxScenario.py +16 -0
- edsl/scenarios/FileStore.py +118 -239
- edsl/scenarios/PdfExtractor.py +40 -0
- edsl/scenarios/Scenario.py +90 -193
- edsl/scenarios/ScenarioHtmlMixin.py +4 -3
- edsl/scenarios/ScenarioJoin.py +10 -6
- edsl/scenarios/ScenarioList.py +383 -240
- edsl/scenarios/ScenarioListExportMixin.py +0 -7
- edsl/scenarios/ScenarioListPdfMixin.py +15 -37
- edsl/scenarios/ScenarioSelector.py +156 -0
- edsl/scenarios/__init__.py +1 -2
- edsl/scenarios/file_methods.py +85 -0
- edsl/scenarios/handlers/__init__.py +13 -0
- edsl/scenarios/handlers/csv.py +38 -0
- edsl/scenarios/handlers/docx.py +76 -0
- edsl/scenarios/handlers/html.py +37 -0
- edsl/scenarios/handlers/json.py +111 -0
- edsl/scenarios/handlers/latex.py +5 -0
- edsl/scenarios/handlers/md.py +51 -0
- edsl/scenarios/handlers/pdf.py +68 -0
- edsl/scenarios/handlers/png.py +39 -0
- edsl/scenarios/handlers/pptx.py +105 -0
- edsl/scenarios/handlers/py.py +294 -0
- edsl/scenarios/handlers/sql.py +313 -0
- edsl/scenarios/handlers/sqlite.py +149 -0
- edsl/scenarios/handlers/txt.py +33 -0
- edsl/study/ObjectEntry.py +1 -1
- edsl/study/SnapShot.py +1 -1
- edsl/study/Study.py +5 -12
- edsl/surveys/ConstructDAG.py +92 -0
- edsl/surveys/EditSurvey.py +221 -0
- edsl/surveys/InstructionHandler.py +100 -0
- edsl/surveys/MemoryManagement.py +72 -0
- edsl/surveys/Rule.py +5 -4
- edsl/surveys/RuleCollection.py +25 -27
- edsl/surveys/RuleManager.py +172 -0
- edsl/surveys/Simulator.py +75 -0
- edsl/surveys/Survey.py +199 -771
- edsl/surveys/SurveyCSS.py +20 -8
- edsl/surveys/{SurveyFlowVisualizationMixin.py → SurveyFlowVisualization.py} +11 -9
- edsl/surveys/SurveyToApp.py +141 -0
- edsl/surveys/__init__.py +4 -2
- edsl/surveys/descriptors.py +6 -2
- edsl/surveys/instructions/ChangeInstruction.py +1 -2
- edsl/surveys/instructions/Instruction.py +4 -13
- edsl/surveys/instructions/InstructionCollection.py +11 -6
- edsl/templates/error_reporting/interview_details.html +1 -1
- edsl/templates/error_reporting/report.html +1 -1
- edsl/tools/plotting.py +1 -1
- edsl/utilities/PrettyList.py +56 -0
- edsl/utilities/is_notebook.py +18 -0
- edsl/utilities/is_valid_variable_name.py +11 -0
- edsl/utilities/remove_edsl_version.py +24 -0
- edsl/utilities/utilities.py +35 -23
- {edsl-0.1.39.dev1.dist-info → edsl-0.1.39.dev2.dist-info}/METADATA +12 -10
- edsl-0.1.39.dev2.dist-info/RECORD +352 -0
- edsl/language_models/KeyLookup.py +0 -30
- edsl/language_models/unused/ReplicateBase.py +0 -83
- edsl/results/ResultsDBMixin.py +0 -238
- edsl-0.1.39.dev1.dist-info/RECORD +0 -277
- {edsl-0.1.39.dev1.dist-info → edsl-0.1.39.dev2.dist-info}/LICENSE +0 -0
- {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
|
-
|
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" =
|
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
|
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
|
-
|
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(
|
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()
|
edsl/jobs/tasks/TaskHistory.py
CHANGED
@@ -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.
|
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
|
-
|
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:
|
@@ -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
|