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.
Files changed (104) hide show
  1. edsl/__init__.py +8 -1
  2. edsl/__init__original.py +134 -0
  3. edsl/__version__.py +1 -1
  4. edsl/agents/agent.py +29 -0
  5. edsl/agents/agent_list.py +36 -1
  6. edsl/base/base_class.py +281 -151
  7. edsl/buckets/__init__.py +8 -3
  8. edsl/buckets/bucket_collection.py +9 -3
  9. edsl/buckets/model_buckets.py +4 -2
  10. edsl/buckets/token_bucket.py +2 -2
  11. edsl/buckets/token_bucket_client.py +5 -3
  12. edsl/caching/cache.py +131 -62
  13. edsl/caching/cache_entry.py +70 -58
  14. edsl/caching/sql_dict.py +17 -0
  15. edsl/cli.py +99 -0
  16. edsl/config/config_class.py +16 -0
  17. edsl/conversation/__init__.py +31 -0
  18. edsl/coop/coop.py +276 -242
  19. edsl/coop/coop_jobs_objects.py +59 -0
  20. edsl/coop/coop_objects.py +29 -0
  21. edsl/coop/coop_regular_objects.py +26 -0
  22. edsl/coop/utils.py +24 -19
  23. edsl/dataset/dataset.py +338 -101
  24. edsl/db_list/sqlite_list.py +349 -0
  25. edsl/inference_services/__init__.py +40 -5
  26. edsl/inference_services/exceptions.py +11 -0
  27. edsl/inference_services/services/anthropic_service.py +5 -2
  28. edsl/inference_services/services/aws_bedrock.py +6 -2
  29. edsl/inference_services/services/azure_ai.py +6 -2
  30. edsl/inference_services/services/google_service.py +3 -2
  31. edsl/inference_services/services/mistral_ai_service.py +6 -2
  32. edsl/inference_services/services/open_ai_service.py +6 -2
  33. edsl/inference_services/services/perplexity_service.py +6 -2
  34. edsl/inference_services/services/test_service.py +105 -7
  35. edsl/interviews/answering_function.py +167 -59
  36. edsl/interviews/interview.py +124 -72
  37. edsl/interviews/interview_task_manager.py +10 -0
  38. edsl/invigilators/invigilators.py +10 -1
  39. edsl/jobs/async_interview_runner.py +146 -104
  40. edsl/jobs/data_structures.py +6 -4
  41. edsl/jobs/decorators.py +61 -0
  42. edsl/jobs/fetch_invigilator.py +61 -18
  43. edsl/jobs/html_table_job_logger.py +14 -2
  44. edsl/jobs/jobs.py +180 -104
  45. edsl/jobs/jobs_component_constructor.py +2 -2
  46. edsl/jobs/jobs_interview_constructor.py +2 -0
  47. edsl/jobs/jobs_pricing_estimation.py +127 -46
  48. edsl/jobs/jobs_remote_inference_logger.py +4 -0
  49. edsl/jobs/jobs_runner_status.py +30 -25
  50. edsl/jobs/progress_bar_manager.py +79 -0
  51. edsl/jobs/remote_inference.py +35 -1
  52. edsl/key_management/key_lookup_builder.py +6 -1
  53. edsl/language_models/language_model.py +102 -12
  54. edsl/language_models/model.py +10 -3
  55. edsl/language_models/price_manager.py +45 -75
  56. edsl/language_models/registry.py +5 -0
  57. edsl/language_models/utilities.py +2 -1
  58. edsl/notebooks/notebook.py +77 -10
  59. edsl/questions/VALIDATION_README.md +134 -0
  60. edsl/questions/__init__.py +24 -1
  61. edsl/questions/exceptions.py +21 -0
  62. edsl/questions/question_check_box.py +171 -149
  63. edsl/questions/question_dict.py +243 -51
  64. edsl/questions/question_multiple_choice_with_other.py +624 -0
  65. edsl/questions/question_registry.py +2 -1
  66. edsl/questions/templates/multiple_choice_with_other/__init__.py +0 -0
  67. edsl/questions/templates/multiple_choice_with_other/answering_instructions.jinja +15 -0
  68. edsl/questions/templates/multiple_choice_with_other/question_presentation.jinja +17 -0
  69. edsl/questions/validation_analysis.py +185 -0
  70. edsl/questions/validation_cli.py +131 -0
  71. edsl/questions/validation_html_report.py +404 -0
  72. edsl/questions/validation_logger.py +136 -0
  73. edsl/results/result.py +63 -16
  74. edsl/results/results.py +702 -171
  75. edsl/scenarios/construct_download_link.py +16 -3
  76. edsl/scenarios/directory_scanner.py +226 -226
  77. edsl/scenarios/file_methods.py +5 -0
  78. edsl/scenarios/file_store.py +117 -6
  79. edsl/scenarios/handlers/__init__.py +5 -1
  80. edsl/scenarios/handlers/mp4_file_store.py +104 -0
  81. edsl/scenarios/handlers/webm_file_store.py +104 -0
  82. edsl/scenarios/scenario.py +120 -101
  83. edsl/scenarios/scenario_list.py +800 -727
  84. edsl/scenarios/scenario_list_gc_test.py +146 -0
  85. edsl/scenarios/scenario_list_memory_test.py +214 -0
  86. edsl/scenarios/scenario_list_source_refactor.md +35 -0
  87. edsl/scenarios/scenario_selector.py +5 -4
  88. edsl/scenarios/scenario_source.py +1990 -0
  89. edsl/scenarios/tests/test_scenario_list_sources.py +52 -0
  90. edsl/surveys/survey.py +22 -0
  91. edsl/tasks/__init__.py +4 -2
  92. edsl/tasks/task_history.py +198 -36
  93. edsl/tests/scenarios/test_ScenarioSource.py +51 -0
  94. edsl/tests/scenarios/test_scenario_list_sources.py +51 -0
  95. edsl/utilities/__init__.py +2 -1
  96. edsl/utilities/decorators.py +121 -0
  97. edsl/utilities/memory_debugger.py +1010 -0
  98. {edsl-0.1.53.dist-info → edsl-0.1.55.dist-info}/METADATA +52 -76
  99. {edsl-0.1.53.dist-info → edsl-0.1.55.dist-info}/RECORD +102 -78
  100. edsl/jobs/jobs_runner_asyncio.py +0 -281
  101. edsl/language_models/unused/fake_openai_service.py +0 -60
  102. {edsl-0.1.53.dist-info → edsl-0.1.55.dist-info}/LICENSE +0 -0
  103. {edsl-0.1.53.dist-info → edsl-0.1.55.dist-info}/WHEEL +0 -0
  104. {edsl-0.1.53.dist-info → edsl-0.1.55.dist-info}/entry_points.txt +0 -0
@@ -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["):-1].strip()
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
- dict_match = re.match(r'(\{.*?\})(.*)', response.strip())
198
- if dict_match:
199
- dict_str, comment = dict_match.groups()
200
- try:
201
- answer_dict = ast.literal_eval(dict_str)
202
- response = {
203
- "answer": answer_dict,
204
- "comment": comment.strip() if comment.strip() else None
205
- }
206
- except (ValueError, SyntaxError):
207
- pass
208
- except Exception:
209
- pass
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(f"Converted '{key}' from {type(value).__name__} to int")
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(f"Converted '{key}' from {type(value).__name__} to float")
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 type_str.startswith("list[") and not isinstance(value, list):
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(self.answer_keys):
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["):-1].strip()
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(value_types: Optional[List[Union[str, type]]]) -> Optional[List[str]]:
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) -> 'QuestionDict':
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) -> 'QuestionDict':
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__":