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