edsl 0.1.53__py3-none-any.whl → 0.1.55__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 +8 -1
- edsl/__init__original.py +134 -0
- edsl/__version__.py +1 -1
- edsl/agents/agent.py +29 -0
- edsl/agents/agent_list.py +36 -1
- edsl/base/base_class.py +281 -151
- edsl/buckets/__init__.py +8 -3
- edsl/buckets/bucket_collection.py +9 -3
- edsl/buckets/model_buckets.py +4 -2
- edsl/buckets/token_bucket.py +2 -2
- edsl/buckets/token_bucket_client.py +5 -3
- edsl/caching/cache.py +131 -62
- edsl/caching/cache_entry.py +70 -58
- edsl/caching/sql_dict.py +17 -0
- edsl/cli.py +99 -0
- edsl/config/config_class.py +16 -0
- edsl/conversation/__init__.py +31 -0
- edsl/coop/coop.py +276 -242
- edsl/coop/coop_jobs_objects.py +59 -0
- edsl/coop/coop_objects.py +29 -0
- edsl/coop/coop_regular_objects.py +26 -0
- edsl/coop/utils.py +24 -19
- edsl/dataset/dataset.py +338 -101
- edsl/db_list/sqlite_list.py +349 -0
- edsl/inference_services/__init__.py +40 -5
- edsl/inference_services/exceptions.py +11 -0
- edsl/inference_services/services/anthropic_service.py +5 -2
- edsl/inference_services/services/aws_bedrock.py +6 -2
- edsl/inference_services/services/azure_ai.py +6 -2
- edsl/inference_services/services/google_service.py +3 -2
- edsl/inference_services/services/mistral_ai_service.py +6 -2
- edsl/inference_services/services/open_ai_service.py +6 -2
- edsl/inference_services/services/perplexity_service.py +6 -2
- edsl/inference_services/services/test_service.py +105 -7
- edsl/interviews/answering_function.py +167 -59
- edsl/interviews/interview.py +124 -72
- edsl/interviews/interview_task_manager.py +10 -0
- edsl/invigilators/invigilators.py +10 -1
- edsl/jobs/async_interview_runner.py +146 -104
- edsl/jobs/data_structures.py +6 -4
- edsl/jobs/decorators.py +61 -0
- edsl/jobs/fetch_invigilator.py +61 -18
- edsl/jobs/html_table_job_logger.py +14 -2
- edsl/jobs/jobs.py +180 -104
- edsl/jobs/jobs_component_constructor.py +2 -2
- edsl/jobs/jobs_interview_constructor.py +2 -0
- edsl/jobs/jobs_pricing_estimation.py +127 -46
- edsl/jobs/jobs_remote_inference_logger.py +4 -0
- edsl/jobs/jobs_runner_status.py +30 -25
- edsl/jobs/progress_bar_manager.py +79 -0
- edsl/jobs/remote_inference.py +35 -1
- edsl/key_management/key_lookup_builder.py +6 -1
- edsl/language_models/language_model.py +102 -12
- edsl/language_models/model.py +10 -3
- edsl/language_models/price_manager.py +45 -75
- edsl/language_models/registry.py +5 -0
- edsl/language_models/utilities.py +2 -1
- edsl/notebooks/notebook.py +77 -10
- edsl/questions/VALIDATION_README.md +134 -0
- edsl/questions/__init__.py +24 -1
- edsl/questions/exceptions.py +21 -0
- edsl/questions/question_check_box.py +171 -149
- edsl/questions/question_dict.py +243 -51
- edsl/questions/question_multiple_choice_with_other.py +624 -0
- edsl/questions/question_registry.py +2 -1
- edsl/questions/templates/multiple_choice_with_other/__init__.py +0 -0
- edsl/questions/templates/multiple_choice_with_other/answering_instructions.jinja +15 -0
- edsl/questions/templates/multiple_choice_with_other/question_presentation.jinja +17 -0
- edsl/questions/validation_analysis.py +185 -0
- edsl/questions/validation_cli.py +131 -0
- edsl/questions/validation_html_report.py +404 -0
- edsl/questions/validation_logger.py +136 -0
- edsl/results/result.py +63 -16
- edsl/results/results.py +702 -171
- edsl/scenarios/construct_download_link.py +16 -3
- edsl/scenarios/directory_scanner.py +226 -226
- edsl/scenarios/file_methods.py +5 -0
- edsl/scenarios/file_store.py +117 -6
- edsl/scenarios/handlers/__init__.py +5 -1
- edsl/scenarios/handlers/mp4_file_store.py +104 -0
- edsl/scenarios/handlers/webm_file_store.py +104 -0
- edsl/scenarios/scenario.py +120 -101
- edsl/scenarios/scenario_list.py +800 -727
- edsl/scenarios/scenario_list_gc_test.py +146 -0
- edsl/scenarios/scenario_list_memory_test.py +214 -0
- edsl/scenarios/scenario_list_source_refactor.md +35 -0
- edsl/scenarios/scenario_selector.py +5 -4
- edsl/scenarios/scenario_source.py +1990 -0
- edsl/scenarios/tests/test_scenario_list_sources.py +52 -0
- edsl/surveys/survey.py +22 -0
- edsl/tasks/__init__.py +4 -2
- edsl/tasks/task_history.py +198 -36
- edsl/tests/scenarios/test_ScenarioSource.py +51 -0
- edsl/tests/scenarios/test_scenario_list_sources.py +51 -0
- edsl/utilities/__init__.py +2 -1
- edsl/utilities/decorators.py +121 -0
- edsl/utilities/memory_debugger.py +1010 -0
- {edsl-0.1.53.dist-info → edsl-0.1.55.dist-info}/METADATA +52 -76
- {edsl-0.1.53.dist-info → edsl-0.1.55.dist-info}/RECORD +102 -78
- edsl/jobs/jobs_runner_asyncio.py +0 -281
- edsl/language_models/unused/fake_openai_service.py +0 -60
- {edsl-0.1.53.dist-info → edsl-0.1.55.dist-info}/LICENSE +0 -0
- {edsl-0.1.53.dist-info → edsl-0.1.55.dist-info}/WHEEL +0 -0
- {edsl-0.1.53.dist-info → edsl-0.1.55.dist-info}/entry_points.txt +0 -0
edsl/questions/question_dict.py
CHANGED
@@ -51,7 +51,7 @@ def _parse_type_string(type_str: str) -> Any:
|
|
51
51
|
return List[Any]
|
52
52
|
elif type_str.startswith("list["):
|
53
53
|
# e.g. "list[str]" or "list[int]" etc.
|
54
|
-
inner = type_str[len("list[")
|
54
|
+
inner = type_str[len("list[") : -1].strip()
|
55
55
|
return List[_parse_type_string(inner)]
|
56
56
|
# If none matched, return a very permissive type or raise an error
|
57
57
|
return Any
|
@@ -74,15 +74,15 @@ def create_dict_response(
|
|
74
74
|
# 1) Build the 'answer' submodel fields
|
75
75
|
# Each key is required (using `...`), with the associated type from value_types.
|
76
76
|
field_definitions = {}
|
77
|
+
if len(value_types) == 0:
|
78
|
+
value_types = ["str"] * len(answer_keys) # Default to str if no types provided
|
77
79
|
for key, t_str in zip(answer_keys, value_types):
|
78
80
|
python_type = _parse_type_string(t_str)
|
79
81
|
field_definitions[key] = (python_type, Field(...))
|
80
82
|
|
81
83
|
# Use Pydantic's create_model to construct an "AnswerSubModel" with these fields
|
82
84
|
AnswerSubModel = create_model(
|
83
|
-
"AnswerSubModel",
|
84
|
-
__base__=BaseModel,
|
85
|
-
**field_definitions
|
85
|
+
"AnswerSubModel", __base__=BaseModel, **field_definitions
|
86
86
|
)
|
87
87
|
|
88
88
|
# 2) Define the top-level model with `answer` + optional `comment`
|
@@ -102,12 +102,12 @@ def create_dict_response(
|
|
102
102
|
class DictResponseValidator(ResponseValidatorABC):
|
103
103
|
"""
|
104
104
|
Validator for dictionary responses with specific keys and value types.
|
105
|
-
|
105
|
+
|
106
106
|
This validator ensures that:
|
107
107
|
1. All required keys are present in the answer
|
108
108
|
2. Each value has the correct type as specified
|
109
109
|
3. Extra keys are forbidden unless permissive=True
|
110
|
-
|
110
|
+
|
111
111
|
Examples:
|
112
112
|
>>> from edsl.questions import QuestionDict
|
113
113
|
>>> q = QuestionDict(
|
@@ -119,7 +119,7 @@ class DictResponseValidator(ResponseValidatorABC):
|
|
119
119
|
>>> validator = q.response_validator
|
120
120
|
>>> result = validator.validate({
|
121
121
|
... "answer": {
|
122
|
-
... "name": "Pancakes",
|
122
|
+
... "name": "Pancakes",
|
123
123
|
... "ingredients": ["flour", "milk", "eggs"],
|
124
124
|
... "steps": ["Mix", "Cook", "Serve"]
|
125
125
|
... }
|
@@ -127,12 +127,13 @@ class DictResponseValidator(ResponseValidatorABC):
|
|
127
127
|
>>> sorted(result.keys())
|
128
128
|
['answer', 'comment', 'generated_tokens']
|
129
129
|
"""
|
130
|
+
|
130
131
|
required_params = ["answer_keys", "permissive"]
|
131
132
|
|
132
133
|
def fix(self, response, verbose=False):
|
133
134
|
"""
|
134
135
|
Attempt to fix an invalid dictionary response.
|
135
|
-
|
136
|
+
|
136
137
|
Examples:
|
137
138
|
>>> # Set up validator with proper response model
|
138
139
|
>>> from pydantic import BaseModel, create_model, Field
|
@@ -151,7 +152,7 @@ class DictResponseValidator(ResponseValidatorABC):
|
|
151
152
|
... permissive=False
|
152
153
|
... )
|
153
154
|
>>> validator.value_types = ["str", "int"]
|
154
|
-
|
155
|
+
|
155
156
|
# Fix dictionary with comment on same line
|
156
157
|
>>> response = "{'name': 'john', 'age': 23} Here you go."
|
157
158
|
>>> result = validator.fix(response)
|
@@ -159,13 +160,13 @@ class DictResponseValidator(ResponseValidatorABC):
|
|
159
160
|
{'name': 'john', 'age': 23}
|
160
161
|
>>> result['comment']
|
161
162
|
'Here you go.'
|
162
|
-
|
163
|
+
|
163
164
|
# Fix type conversion (string to int)
|
164
165
|
>>> response = {"answer": {"name": "john", "age": "23"}}
|
165
166
|
>>> result = validator.fix(response)
|
166
167
|
>>> dict(result['answer']) # Convert to dict for consistent output
|
167
168
|
{'name': 'john', 'age': 23}
|
168
|
-
|
169
|
+
|
169
170
|
# Fix list from comma-separated string
|
170
171
|
>>> AnswerModel2 = create_model('AnswerModel2', name=(str, ...), hobbies=(List[str], ...))
|
171
172
|
>>> ResponseModel2 = create_model(
|
@@ -184,39 +185,189 @@ class DictResponseValidator(ResponseValidatorABC):
|
|
184
185
|
>>> result = validator.fix(response)
|
185
186
|
>>> dict(result['answer']) # Convert to dict for consistent output
|
186
187
|
{'name': 'john', 'hobbies': ['reading', 'gaming', 'coding']}
|
187
|
-
|
188
|
+
|
188
189
|
# Handle invalid input gracefully
|
189
190
|
>>> response = "not a dictionary"
|
190
191
|
>>> validator.fix(response)
|
191
192
|
'not a dictionary'
|
192
193
|
"""
|
193
194
|
# First try to separate dictionary from trailing comment if they're on the same line
|
195
|
+
original_response = response
|
194
196
|
if isinstance(response, str):
|
195
197
|
# Try to find where the dictionary ends and comment begins
|
196
198
|
try:
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
199
|
+
# Find the first opening brace
|
200
|
+
response_str = response.strip()
|
201
|
+
if response_str.startswith("{"):
|
202
|
+
# Count braces to find proper JSON ending
|
203
|
+
brace_count = 0
|
204
|
+
dict_end_pos = None
|
205
|
+
|
206
|
+
for i, char in enumerate(response_str):
|
207
|
+
if char == "{":
|
208
|
+
brace_count += 1
|
209
|
+
elif char == "}":
|
210
|
+
brace_count -= 1
|
211
|
+
if brace_count == 0:
|
212
|
+
dict_end_pos = i + 1
|
213
|
+
break
|
214
|
+
|
215
|
+
if dict_end_pos is not None:
|
216
|
+
dict_str = response_str[:dict_end_pos]
|
217
|
+
comment = response_str[dict_end_pos:].strip()
|
218
|
+
|
219
|
+
try:
|
220
|
+
answer_dict = ast.literal_eval(dict_str)
|
221
|
+
response = {
|
222
|
+
"answer": answer_dict,
|
223
|
+
"comment": comment if comment else None,
|
224
|
+
}
|
225
|
+
if verbose:
|
226
|
+
print(
|
227
|
+
f"Successfully split answer from comment. Comment length: {len(comment) if comment else 0}"
|
228
|
+
)
|
229
|
+
except (ValueError, SyntaxError) as e:
|
230
|
+
if verbose:
|
231
|
+
print(f"Failed to parse dictionary: {e}")
|
232
|
+
except Exception as e:
|
233
|
+
if verbose:
|
234
|
+
print(f"Exception during dictionary parsing: {e}")
|
210
235
|
|
211
236
|
# Continue with existing fix logic
|
212
237
|
if "answer" not in response or not isinstance(response["answer"], dict):
|
213
238
|
if verbose:
|
214
239
|
print("Cannot fix response: 'answer' field missing or not a dictionary")
|
240
|
+
|
241
|
+
# Special case: if we have the original string response, try a more direct parsing approach
|
242
|
+
if isinstance(
|
243
|
+
original_response, str
|
244
|
+
) and original_response.strip().startswith("{"):
|
245
|
+
try:
|
246
|
+
# Try to parse the JSON part directly, skipping nested comments
|
247
|
+
response_str = original_response.strip()
|
248
|
+
import json
|
249
|
+
|
250
|
+
# Find where the dict ends by tracking nested braces
|
251
|
+
brace_count = 0
|
252
|
+
dict_end_pos = None
|
253
|
+
|
254
|
+
for i, char in enumerate(response_str):
|
255
|
+
if char == "{":
|
256
|
+
brace_count += 1
|
257
|
+
elif char == "}":
|
258
|
+
brace_count -= 1
|
259
|
+
if brace_count == 0:
|
260
|
+
dict_end_pos = i + 1
|
261
|
+
break
|
262
|
+
|
263
|
+
if dict_end_pos is not None:
|
264
|
+
dict_str = response_str[:dict_end_pos]
|
265
|
+
comment = response_str[dict_end_pos:].strip()
|
266
|
+
|
267
|
+
# Try parsing with JSON first (faster but stricter)
|
268
|
+
try:
|
269
|
+
dict_str = dict_str.replace(
|
270
|
+
"'", '"'
|
271
|
+
) # Convert Python quotes to JSON quotes
|
272
|
+
dict_str = dict_str.replace("False", "false").replace(
|
273
|
+
"True", "true"
|
274
|
+
) # Fix booleans
|
275
|
+
answer_dict = json.loads(dict_str)
|
276
|
+
except json.JSONDecodeError:
|
277
|
+
# Fall back to ast.literal_eval (safer)
|
278
|
+
try:
|
279
|
+
answer_dict = ast.literal_eval(dict_str)
|
280
|
+
except (ValueError, SyntaxError):
|
281
|
+
if verbose:
|
282
|
+
print("Could not parse the dictionary part")
|
283
|
+
return original_response
|
284
|
+
|
285
|
+
# Now fix types
|
286
|
+
fixed_answer = {}
|
287
|
+
for key, type_str in zip(
|
288
|
+
self.answer_keys, getattr(self, "value_types", [])
|
289
|
+
):
|
290
|
+
if key in answer_dict:
|
291
|
+
value = answer_dict[key]
|
292
|
+
# Convert types
|
293
|
+
if type_str == "int" and not isinstance(value, int):
|
294
|
+
try:
|
295
|
+
fixed_answer[key] = int(value)
|
296
|
+
if verbose:
|
297
|
+
print(
|
298
|
+
f"Converted '{key}' from {type(value).__name__} to int"
|
299
|
+
)
|
300
|
+
except (ValueError, TypeError):
|
301
|
+
fixed_answer[key] = value
|
302
|
+
|
303
|
+
elif type_str == "float" and not isinstance(
|
304
|
+
value, float
|
305
|
+
):
|
306
|
+
try:
|
307
|
+
fixed_answer[key] = float(value)
|
308
|
+
if verbose:
|
309
|
+
print(
|
310
|
+
f"Converted '{key}' from {type(value).__name__} to float"
|
311
|
+
)
|
312
|
+
except (ValueError, TypeError):
|
313
|
+
fixed_answer[key] = value
|
314
|
+
|
315
|
+
elif (
|
316
|
+
type_str.startswith("list[") or type_str == "list"
|
317
|
+
) and not isinstance(value, list):
|
318
|
+
# Convert string to list by splitting
|
319
|
+
if isinstance(value, str):
|
320
|
+
items = [
|
321
|
+
item.strip() for item in value.split(",")
|
322
|
+
]
|
323
|
+
fixed_answer[key] = items
|
324
|
+
if verbose:
|
325
|
+
print(
|
326
|
+
f"Converted '{key}' from string to list: {items}"
|
327
|
+
)
|
328
|
+
else:
|
329
|
+
fixed_answer[key] = value
|
330
|
+
else:
|
331
|
+
fixed_answer[key] = value
|
332
|
+
else:
|
333
|
+
# Key not in answer, set a default
|
334
|
+
if type_str == "int":
|
335
|
+
fixed_answer[key] = 0
|
336
|
+
elif type_str == "float":
|
337
|
+
fixed_answer[key] = 0.0
|
338
|
+
elif type_str.startswith("list") or type_str == "list":
|
339
|
+
fixed_answer[key] = []
|
340
|
+
else:
|
341
|
+
fixed_answer[key] = ""
|
342
|
+
|
343
|
+
# Construct final fixed response
|
344
|
+
fixed_response = {
|
345
|
+
"answer": fixed_answer,
|
346
|
+
"comment": comment if comment else None,
|
347
|
+
"generated_tokens": None,
|
348
|
+
}
|
349
|
+
|
350
|
+
if verbose:
|
351
|
+
print(f"Directly fixed response with type conversion")
|
352
|
+
|
353
|
+
try:
|
354
|
+
# Try to validate
|
355
|
+
self.response_model.model_validate(fixed_response)
|
356
|
+
if verbose:
|
357
|
+
print("Successfully validated fixed response")
|
358
|
+
return fixed_response
|
359
|
+
except Exception as e:
|
360
|
+
if verbose:
|
361
|
+
print(f"Validation of direct fix failed: {e}")
|
362
|
+
except Exception as e:
|
363
|
+
if verbose:
|
364
|
+
print(f"Error during direct parsing: {e}")
|
365
|
+
|
215
366
|
return response
|
216
|
-
|
367
|
+
|
217
368
|
answer_dict = response["answer"]
|
218
369
|
fixed_answer = {}
|
219
|
-
|
370
|
+
|
220
371
|
# Try to convert values to expected types
|
221
372
|
for key, type_str in zip(self.answer_keys, getattr(self, "value_types", [])):
|
222
373
|
if key in answer_dict:
|
@@ -226,21 +377,27 @@ class DictResponseValidator(ResponseValidatorABC):
|
|
226
377
|
try:
|
227
378
|
fixed_answer[key] = int(value)
|
228
379
|
if verbose:
|
229
|
-
print(
|
380
|
+
print(
|
381
|
+
f"Converted '{key}' from {type(value).__name__} to int"
|
382
|
+
)
|
230
383
|
continue
|
231
384
|
except (ValueError, TypeError):
|
232
385
|
pass
|
233
|
-
|
386
|
+
|
234
387
|
elif type_str == "float" and not isinstance(value, float):
|
235
388
|
try:
|
236
389
|
fixed_answer[key] = float(value)
|
237
390
|
if verbose:
|
238
|
-
print(
|
391
|
+
print(
|
392
|
+
f"Converted '{key}' from {type(value).__name__} to float"
|
393
|
+
)
|
239
394
|
continue
|
240
395
|
except (ValueError, TypeError):
|
241
396
|
pass
|
242
|
-
|
243
|
-
elif
|
397
|
+
|
398
|
+
elif (
|
399
|
+
type_str.startswith("list[") or type_str == "list"
|
400
|
+
) and not isinstance(value, list):
|
244
401
|
# Try to convert string to list by splitting
|
245
402
|
if isinstance(value, str):
|
246
403
|
items = [item.strip() for item in value.split(",")]
|
@@ -248,22 +405,23 @@ class DictResponseValidator(ResponseValidatorABC):
|
|
248
405
|
if verbose:
|
249
406
|
print(f"Converted '{key}' from string to list: {items}")
|
250
407
|
continue
|
251
|
-
|
408
|
+
|
252
409
|
# If no conversion needed or possible, keep original
|
253
410
|
fixed_answer[key] = value
|
254
|
-
|
411
|
+
|
255
412
|
# Preserve any keys we didn't try to fix
|
256
413
|
for key, value in answer_dict.items():
|
257
414
|
if key not in fixed_answer:
|
258
415
|
fixed_answer[key] = value
|
259
|
-
|
416
|
+
|
260
417
|
# Return fixed response
|
261
418
|
fixed_response = {
|
262
419
|
"answer": fixed_answer,
|
263
420
|
"comment": response.get("comment"),
|
264
421
|
"generated_tokens": response.get("generated_tokens")
|
422
|
+
or response, # Ensure generated_tokens is captured
|
265
423
|
}
|
266
|
-
|
424
|
+
|
267
425
|
try:
|
268
426
|
# Validate the fixed answer
|
269
427
|
self.response_model.model_validate(fixed_response)
|
@@ -273,6 +431,39 @@ class DictResponseValidator(ResponseValidatorABC):
|
|
273
431
|
except Exception as e:
|
274
432
|
if verbose:
|
275
433
|
print(f"Validation failed for fixed answer: {e}")
|
434
|
+
|
435
|
+
# If still failing, try one more time with default values for missing keys
|
436
|
+
if hasattr(self, "answer_keys") and hasattr(self, "value_types"):
|
437
|
+
for key, type_str in zip(
|
438
|
+
self.answer_keys, getattr(self, "value_types", [])
|
439
|
+
):
|
440
|
+
if key not in fixed_answer:
|
441
|
+
if type_str == "int":
|
442
|
+
fixed_answer[key] = 0
|
443
|
+
elif type_str == "float":
|
444
|
+
fixed_answer[key] = 0.0
|
445
|
+
elif type_str.startswith("list") or type_str == "list":
|
446
|
+
fixed_answer[key] = []
|
447
|
+
else:
|
448
|
+
fixed_answer[key] = ""
|
449
|
+
|
450
|
+
# Try again with all keys
|
451
|
+
fixed_response = {
|
452
|
+
"answer": fixed_answer,
|
453
|
+
"comment": response.get("comment"),
|
454
|
+
"generated_tokens": response.get("generated_tokens"),
|
455
|
+
}
|
456
|
+
|
457
|
+
try:
|
458
|
+
# Validate the fixed answer
|
459
|
+
self.response_model.model_validate(fixed_response)
|
460
|
+
if verbose:
|
461
|
+
print("Successfully fixed response with defaults")
|
462
|
+
return fixed_response
|
463
|
+
except Exception as e:
|
464
|
+
if verbose:
|
465
|
+
print(f"Validation still failed after adding defaults: {e}")
|
466
|
+
|
276
467
|
return response
|
277
468
|
|
278
469
|
valid_examples = [
|
@@ -281,12 +472,12 @@ class DictResponseValidator(ResponseValidatorABC):
|
|
281
472
|
"answer": {
|
282
473
|
"name": "Hot Chocolate",
|
283
474
|
"num_ingredients": 5,
|
284
|
-
"ingredients": ["milk", "cocoa", "sugar"]
|
475
|
+
"ingredients": ["milk", "cocoa", "sugar"],
|
285
476
|
}
|
286
477
|
},
|
287
478
|
{
|
288
479
|
"answer_keys": ["name", "num_ingredients", "ingredients"],
|
289
|
-
"value_types": ["str", "int", "list[str]"]
|
480
|
+
"value_types": ["str", "int", "list[str]"],
|
290
481
|
},
|
291
482
|
)
|
292
483
|
]
|
@@ -300,7 +491,7 @@ class DictResponseValidator(ResponseValidatorABC):
|
|
300
491
|
{"answer": {"ingredients": "milk"}}, # Should be a list
|
301
492
|
{"answer_keys": ["ingredients"], "value_types": ["list[str]"]},
|
302
493
|
"Key 'ingredients' should be a list, got str",
|
303
|
-
)
|
494
|
+
),
|
304
495
|
]
|
305
496
|
|
306
497
|
|
@@ -373,7 +564,9 @@ class QuestionDict(QuestionBase):
|
|
373
564
|
raise QuestionCreationValidationError(
|
374
565
|
"Length of value_types must match length of answer_keys."
|
375
566
|
)
|
376
|
-
if self.value_descriptions and len(self.value_descriptions) != len(
|
567
|
+
if self.value_descriptions and len(self.value_descriptions) != len(
|
568
|
+
self.answer_keys
|
569
|
+
):
|
377
570
|
raise QuestionCreationValidationError(
|
378
571
|
"Length of value_descriptions must match length of answer_keys."
|
379
572
|
)
|
@@ -386,7 +579,7 @@ class QuestionDict(QuestionBase):
|
|
386
579
|
return create_dict_response(
|
387
580
|
answer_keys=self.answer_keys,
|
388
581
|
value_types=self.value_types or [],
|
389
|
-
permissive=self.permissive
|
582
|
+
permissive=self.permissive,
|
390
583
|
)
|
391
584
|
|
392
585
|
def _get_default_answer(self) -> Dict[str, Any]:
|
@@ -397,7 +590,7 @@ class QuestionDict(QuestionBase):
|
|
397
590
|
"title": "Sample Recipe",
|
398
591
|
"ingredients": ["ingredient1", "ingredient2"],
|
399
592
|
"num_ingredients": 2,
|
400
|
-
"instructions": "Sample instructions"
|
593
|
+
"instructions": "Sample instructions",
|
401
594
|
}
|
402
595
|
|
403
596
|
answer = {}
|
@@ -405,7 +598,7 @@ class QuestionDict(QuestionBase):
|
|
405
598
|
t_str = type_str.lower()
|
406
599
|
if t_str.startswith("list["):
|
407
600
|
# e.g. list[str], list[int], etc.
|
408
|
-
inner = t_str[len("list[")
|
601
|
+
inner = t_str[len("list[") : -1].strip()
|
409
602
|
if inner == "str":
|
410
603
|
answer[key] = ["sample_string"]
|
411
604
|
elif inner == "int":
|
@@ -445,7 +638,9 @@ class QuestionDict(QuestionBase):
|
|
445
638
|
return f"Template {template_name} not found in {template_dir}."
|
446
639
|
|
447
640
|
@staticmethod
|
448
|
-
def _normalize_value_types(
|
641
|
+
def _normalize_value_types(
|
642
|
+
value_types: Optional[List[Union[str, type]]]
|
643
|
+
) -> Optional[List[str]]:
|
449
644
|
"""
|
450
645
|
Convert all value_types to string representations (e.g. "int", "list[str]", etc.).
|
451
646
|
This logic is similar to your original approach but expanded to handle
|
@@ -486,7 +681,7 @@ class QuestionDict(QuestionBase):
|
|
486
681
|
}
|
487
682
|
|
488
683
|
@classmethod
|
489
|
-
def from_dict(cls, data: dict) ->
|
684
|
+
def from_dict(cls, data: dict) -> "QuestionDict":
|
490
685
|
"""Recreate from a dictionary."""
|
491
686
|
return cls(
|
492
687
|
question_name=data["question_name"],
|
@@ -500,7 +695,7 @@ class QuestionDict(QuestionBase):
|
|
500
695
|
|
501
696
|
@classmethod
|
502
697
|
@inject_exception
|
503
|
-
def example(cls) ->
|
698
|
+
def example(cls) -> "QuestionDict":
|
504
699
|
"""Return an example question."""
|
505
700
|
return cls(
|
506
701
|
question_name="example",
|
@@ -511,16 +706,13 @@ class QuestionDict(QuestionBase):
|
|
511
706
|
"The title of the recipe.",
|
512
707
|
"A list of ingredients.",
|
513
708
|
"The number of ingredients.",
|
514
|
-
"The instructions for making the recipe."
|
709
|
+
"The instructions for making the recipe.",
|
515
710
|
],
|
516
711
|
)
|
517
712
|
|
518
713
|
def _simulate_answer(self) -> dict:
|
519
714
|
"""Simulate an answer for the question."""
|
520
|
-
return {
|
521
|
-
"answer": self._get_default_answer(),
|
522
|
-
"comment": None
|
523
|
-
}
|
715
|
+
return {"answer": self._get_default_answer(), "comment": None}
|
524
716
|
|
525
717
|
|
526
718
|
if __name__ == "__main__":
|