edsl 0.1.48__py3-none-any.whl → 0.1.50__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 (239) hide show
  1. edsl/__init__.py +124 -53
  2. edsl/__version__.py +1 -1
  3. edsl/agents/agent.py +21 -21
  4. edsl/agents/agent_list.py +2 -5
  5. edsl/agents/exceptions.py +119 -5
  6. edsl/base/__init__.py +10 -35
  7. edsl/base/base_class.py +71 -36
  8. edsl/base/base_exception.py +204 -0
  9. edsl/base/data_transfer_models.py +1 -1
  10. edsl/base/exceptions.py +94 -0
  11. edsl/buckets/__init__.py +15 -1
  12. edsl/buckets/bucket_collection.py +3 -4
  13. edsl/buckets/exceptions.py +75 -0
  14. edsl/buckets/model_buckets.py +1 -2
  15. edsl/buckets/token_bucket.py +11 -6
  16. edsl/buckets/token_bucket_api.py +1 -2
  17. edsl/buckets/token_bucket_client.py +9 -7
  18. edsl/caching/cache.py +7 -2
  19. edsl/caching/cache_entry.py +10 -9
  20. edsl/caching/exceptions.py +113 -7
  21. edsl/caching/remote_cache_sync.py +1 -2
  22. edsl/caching/sql_dict.py +17 -12
  23. edsl/cli.py +43 -0
  24. edsl/config/config_class.py +30 -6
  25. edsl/conversation/Conversation.py +3 -2
  26. edsl/conversation/exceptions.py +58 -0
  27. edsl/conversation/mug_negotiation.py +0 -2
  28. edsl/coop/__init__.py +20 -1
  29. edsl/coop/coop.py +129 -38
  30. edsl/coop/exceptions.py +188 -9
  31. edsl/coop/price_fetcher.py +3 -6
  32. edsl/coop/utils.py +4 -6
  33. edsl/dataset/__init__.py +5 -4
  34. edsl/dataset/dataset.py +53 -43
  35. edsl/dataset/dataset_operations_mixin.py +86 -72
  36. edsl/dataset/dataset_tree.py +9 -5
  37. edsl/dataset/display/table_display.py +0 -2
  38. edsl/dataset/display/table_renderers.py +0 -1
  39. edsl/dataset/exceptions.py +125 -0
  40. edsl/dataset/file_exports.py +18 -11
  41. edsl/dataset/r/ggplot.py +13 -6
  42. edsl/display/__init__.py +27 -0
  43. edsl/display/core.py +147 -0
  44. edsl/display/plugin.py +189 -0
  45. edsl/display/utils.py +52 -0
  46. edsl/inference_services/__init__.py +9 -1
  47. edsl/inference_services/available_model_cache_handler.py +1 -1
  48. edsl/inference_services/available_model_fetcher.py +4 -5
  49. edsl/inference_services/data_structures.py +9 -6
  50. edsl/inference_services/exceptions.py +132 -1
  51. edsl/inference_services/inference_service_abc.py +2 -2
  52. edsl/inference_services/inference_services_collection.py +2 -6
  53. edsl/inference_services/registry.py +4 -3
  54. edsl/inference_services/service_availability.py +2 -1
  55. edsl/inference_services/services/anthropic_service.py +4 -1
  56. edsl/inference_services/services/aws_bedrock.py +13 -12
  57. edsl/inference_services/services/azure_ai.py +12 -10
  58. edsl/inference_services/services/deep_infra_service.py +1 -4
  59. edsl/inference_services/services/deep_seek_service.py +1 -5
  60. edsl/inference_services/services/google_service.py +6 -2
  61. edsl/inference_services/services/groq_service.py +1 -1
  62. edsl/inference_services/services/mistral_ai_service.py +4 -2
  63. edsl/inference_services/services/ollama_service.py +1 -1
  64. edsl/inference_services/services/open_ai_service.py +7 -5
  65. edsl/inference_services/services/perplexity_service.py +6 -2
  66. edsl/inference_services/services/test_service.py +8 -7
  67. edsl/inference_services/services/together_ai_service.py +2 -3
  68. edsl/inference_services/services/xai_service.py +1 -1
  69. edsl/instructions/__init__.py +1 -1
  70. edsl/instructions/change_instruction.py +3 -2
  71. edsl/instructions/exceptions.py +61 -0
  72. edsl/instructions/instruction.py +5 -2
  73. edsl/instructions/instruction_collection.py +2 -1
  74. edsl/instructions/instruction_handler.py +4 -9
  75. edsl/interviews/ReportErrors.py +0 -3
  76. edsl/interviews/__init__.py +9 -2
  77. edsl/interviews/answering_function.py +11 -13
  78. edsl/interviews/exception_tracking.py +14 -7
  79. edsl/interviews/exceptions.py +79 -0
  80. edsl/interviews/interview.py +32 -29
  81. edsl/interviews/interview_status_dictionary.py +4 -2
  82. edsl/interviews/interview_status_log.py +2 -1
  83. edsl/interviews/interview_task_manager.py +3 -3
  84. edsl/interviews/request_token_estimator.py +3 -1
  85. edsl/interviews/statistics.py +2 -3
  86. edsl/invigilators/__init__.py +7 -1
  87. edsl/invigilators/exceptions.py +79 -0
  88. edsl/invigilators/invigilator_base.py +0 -1
  89. edsl/invigilators/invigilators.py +8 -12
  90. edsl/invigilators/prompt_constructor.py +1 -5
  91. edsl/invigilators/prompt_helpers.py +8 -4
  92. edsl/invigilators/question_instructions_prompt_builder.py +1 -1
  93. edsl/invigilators/question_option_processor.py +9 -5
  94. edsl/invigilators/question_template_replacements_builder.py +3 -2
  95. edsl/jobs/__init__.py +3 -3
  96. edsl/jobs/async_interview_runner.py +24 -22
  97. edsl/jobs/check_survey_scenario_compatibility.py +7 -6
  98. edsl/jobs/data_structures.py +7 -4
  99. edsl/jobs/exceptions.py +177 -8
  100. edsl/jobs/fetch_invigilator.py +1 -1
  101. edsl/jobs/jobs.py +72 -67
  102. edsl/jobs/jobs_checks.py +2 -3
  103. edsl/jobs/jobs_component_constructor.py +2 -2
  104. edsl/jobs/jobs_pricing_estimation.py +3 -2
  105. edsl/jobs/jobs_remote_inference_logger.py +5 -4
  106. edsl/jobs/jobs_runner_asyncio.py +1 -2
  107. edsl/jobs/jobs_runner_status.py +8 -9
  108. edsl/jobs/remote_inference.py +26 -23
  109. edsl/jobs/results_exceptions_handler.py +8 -5
  110. edsl/key_management/__init__.py +3 -1
  111. edsl/key_management/exceptions.py +62 -0
  112. edsl/key_management/key_lookup.py +1 -1
  113. edsl/key_management/key_lookup_builder.py +37 -14
  114. edsl/key_management/key_lookup_collection.py +2 -0
  115. edsl/language_models/__init__.py +1 -1
  116. edsl/language_models/exceptions.py +302 -14
  117. edsl/language_models/language_model.py +4 -7
  118. edsl/language_models/model.py +4 -4
  119. edsl/language_models/model_list.py +1 -1
  120. edsl/language_models/price_manager.py +1 -1
  121. edsl/language_models/raw_response_handler.py +14 -9
  122. edsl/language_models/registry.py +17 -21
  123. edsl/language_models/repair.py +0 -6
  124. edsl/language_models/unused/fake_openai_service.py +0 -1
  125. edsl/load_plugins.py +69 -0
  126. edsl/logger.py +146 -0
  127. edsl/notebooks/notebook.py +1 -1
  128. edsl/notebooks/notebook_to_latex.py +0 -1
  129. edsl/plugins/__init__.py +63 -0
  130. edsl/plugins/built_in/export_example.py +50 -0
  131. edsl/plugins/built_in/pig_latin.py +67 -0
  132. edsl/plugins/cli.py +372 -0
  133. edsl/plugins/cli_typer.py +283 -0
  134. edsl/plugins/exceptions.py +31 -0
  135. edsl/plugins/hookspec.py +51 -0
  136. edsl/plugins/plugin_host.py +128 -0
  137. edsl/plugins/plugin_manager.py +633 -0
  138. edsl/plugins/plugins_registry.py +168 -0
  139. edsl/prompts/__init__.py +2 -0
  140. edsl/prompts/exceptions.py +107 -5
  141. edsl/prompts/prompt.py +14 -6
  142. edsl/questions/HTMLQuestion.py +5 -11
  143. edsl/questions/Quick.py +0 -1
  144. edsl/questions/__init__.py +2 -0
  145. edsl/questions/answer_validator_mixin.py +318 -318
  146. edsl/questions/compose_questions.py +2 -2
  147. edsl/questions/descriptors.py +10 -49
  148. edsl/questions/exceptions.py +278 -22
  149. edsl/questions/loop_processor.py +7 -5
  150. edsl/questions/prompt_templates/question_list.jinja +3 -0
  151. edsl/questions/question_base.py +14 -16
  152. edsl/questions/question_base_gen_mixin.py +2 -2
  153. edsl/questions/question_base_prompts_mixin.py +9 -3
  154. edsl/questions/question_budget.py +9 -5
  155. edsl/questions/question_check_box.py +3 -5
  156. edsl/questions/question_dict.py +171 -194
  157. edsl/questions/question_extract.py +1 -1
  158. edsl/questions/question_free_text.py +4 -6
  159. edsl/questions/question_functional.py +4 -3
  160. edsl/questions/question_list.py +36 -9
  161. edsl/questions/question_matrix.py +95 -61
  162. edsl/questions/question_multiple_choice.py +6 -4
  163. edsl/questions/question_numerical.py +2 -4
  164. edsl/questions/question_registry.py +4 -2
  165. edsl/questions/register_questions_meta.py +0 -1
  166. edsl/questions/response_validator_abc.py +7 -13
  167. edsl/questions/templates/dict/answering_instructions.jinja +1 -0
  168. edsl/questions/templates/rank/question_presentation.jinja +1 -1
  169. edsl/results/__init__.py +1 -1
  170. edsl/results/exceptions.py +141 -7
  171. edsl/results/report.py +0 -1
  172. edsl/results/result.py +4 -5
  173. edsl/results/results.py +10 -51
  174. edsl/results/results_selector.py +8 -4
  175. edsl/scenarios/PdfExtractor.py +2 -2
  176. edsl/scenarios/construct_download_link.py +69 -35
  177. edsl/scenarios/directory_scanner.py +33 -14
  178. edsl/scenarios/document_chunker.py +1 -1
  179. edsl/scenarios/exceptions.py +238 -14
  180. edsl/scenarios/file_methods.py +1 -1
  181. edsl/scenarios/file_store.py +7 -3
  182. edsl/scenarios/handlers/__init__.py +17 -0
  183. edsl/scenarios/handlers/docx_file_store.py +0 -5
  184. edsl/scenarios/handlers/pdf_file_store.py +0 -1
  185. edsl/scenarios/handlers/pptx_file_store.py +0 -5
  186. edsl/scenarios/handlers/py_file_store.py +0 -1
  187. edsl/scenarios/handlers/sql_file_store.py +1 -4
  188. edsl/scenarios/handlers/sqlite_file_store.py +0 -1
  189. edsl/scenarios/handlers/txt_file_store.py +1 -1
  190. edsl/scenarios/scenario.py +0 -1
  191. edsl/scenarios/scenario_list.py +152 -18
  192. edsl/scenarios/scenario_list_pdf_tools.py +1 -0
  193. edsl/scenarios/scenario_selector.py +0 -1
  194. edsl/surveys/__init__.py +3 -4
  195. edsl/surveys/dag/__init__.py +4 -2
  196. edsl/surveys/descriptors.py +1 -1
  197. edsl/surveys/edit_survey.py +1 -0
  198. edsl/surveys/exceptions.py +165 -9
  199. edsl/surveys/memory/__init__.py +5 -3
  200. edsl/surveys/memory/memory_management.py +1 -0
  201. edsl/surveys/memory/memory_plan.py +6 -15
  202. edsl/surveys/rules/__init__.py +5 -3
  203. edsl/surveys/rules/rule.py +1 -2
  204. edsl/surveys/rules/rule_collection.py +1 -1
  205. edsl/surveys/survey.py +12 -24
  206. edsl/surveys/survey_export.py +6 -3
  207. edsl/surveys/survey_flow_visualization.py +10 -1
  208. edsl/tasks/__init__.py +2 -0
  209. edsl/tasks/question_task_creator.py +3 -3
  210. edsl/tasks/task_creators.py +1 -3
  211. edsl/tasks/task_history.py +5 -7
  212. edsl/tasks/task_status_log.py +1 -2
  213. edsl/tokens/__init__.py +3 -1
  214. edsl/tokens/token_usage.py +1 -1
  215. edsl/utilities/__init__.py +21 -1
  216. edsl/utilities/decorators.py +1 -2
  217. edsl/utilities/markdown_to_docx.py +2 -2
  218. edsl/utilities/markdown_to_pdf.py +1 -1
  219. edsl/utilities/repair_functions.py +0 -1
  220. edsl/utilities/restricted_python.py +0 -1
  221. edsl/utilities/template_loader.py +2 -3
  222. edsl/utilities/utilities.py +8 -29
  223. {edsl-0.1.48.dist-info → edsl-0.1.50.dist-info}/METADATA +32 -2
  224. edsl-0.1.50.dist-info/RECORD +363 -0
  225. edsl-0.1.50.dist-info/entry_points.txt +3 -0
  226. edsl/dataset/smart_objects.py +0 -96
  227. edsl/exceptions/BaseException.py +0 -21
  228. edsl/exceptions/__init__.py +0 -54
  229. edsl/exceptions/configuration.py +0 -16
  230. edsl/exceptions/general.py +0 -34
  231. edsl/study/ObjectEntry.py +0 -173
  232. edsl/study/ProofOfWork.py +0 -113
  233. edsl/study/SnapShot.py +0 -80
  234. edsl/study/Study.py +0 -520
  235. edsl/study/__init__.py +0 -6
  236. edsl/utilities/interface.py +0 -135
  237. edsl-0.1.48.dist-info/RECORD +0 -347
  238. {edsl-0.1.48.dist-info → edsl-0.1.50.dist-info}/LICENSE +0 -0
  239. {edsl-0.1.48.dist-info → edsl-0.1.50.dist-info}/WHEEL +0 -0
@@ -3,7 +3,6 @@
3
3
  from .question_functional import QuestionFunctional
4
4
  from .question_base import QuestionBase
5
5
  from ..scenarios import Scenario
6
- from ..agents import Agent
7
6
 
8
7
 
9
8
  def compose_questions(
@@ -20,7 +19,8 @@ def compose_questions(
20
19
  if question_name is None:
21
20
  question_name = f"{q1.question_name}_{q2.question_name}"
22
21
  if q1.question_name not in q2.question_text:
23
- raise ValueError(
22
+ from .exceptions import QuestionValueError
23
+ raise QuestionValueError(
24
24
  f"q2 requires a field not present in q1's answer. "
25
25
  f"q1: {q1.question_name}, q2: {q2.question_name}"
26
26
  )
@@ -2,7 +2,7 @@
2
2
 
3
3
  from abc import ABC, abstractmethod
4
4
  import re
5
- from typing import Any, Callable, List, Optional
5
+ from typing import Any, Callable, List
6
6
  from .exceptions import (
7
7
  QuestionCreationValidationError,
8
8
  QuestionAnswerValidationError,
@@ -243,7 +243,7 @@ class QuestionNameDescriptor(BaseDescriptor):
243
243
 
244
244
  if not is_valid_variable_name(value):
245
245
  raise QuestionCreationValidationError(
246
- f"`question_name` is not a valid variable name (got {value})."
246
+ f"`question_name` is not a valid variable name (got '{value}')."
247
247
  )
248
248
 
249
249
 
@@ -257,18 +257,11 @@ class QuestionOptionsDescriptor(BaseDescriptor):
257
257
  ... assert len(w) == 1
258
258
  ... assert "trailing whitespace" in str(w[0].message)
259
259
 
260
- >>> _ = q_class(["a", "b", "c", "d", "d"])
261
- Traceback (most recent call last):
262
- ...
263
- edsl.questions.exceptions.QuestionCreationValidationError: Question options must be unique (got ['a', 'b', 'c', 'd', 'd']).
260
+ # Duplicate options would raise QuestionCreationValidationError
264
261
 
265
262
  We allow dynamic question options, which are strings of the form '{{ question_options }}'.
266
263
 
267
264
  >>> _ = q_class("{{dynamic_options}}")
268
- >>> _ = q_class("dynamic_options")
269
- Traceback (most recent call last):
270
- ...
271
- edsl.questions.exceptions.QuestionCreationValidationError: ...
272
265
  """
273
266
 
274
267
  @classmethod
@@ -306,10 +299,6 @@ class QuestionOptionsDescriptor(BaseDescriptor):
306
299
  raise QuestionCreationValidationError(
307
300
  f"Question options must be a list (got {value})."
308
301
  )
309
- # if len(value) > Settings.MAX_NUM_OPTIONS:
310
- # raise QuestionCreationValidationError(
311
- # f"Too many question options (got {value})."
312
- # )
313
302
  if len(value) < Settings.MIN_NUM_OPTIONS:
314
303
  raise QuestionCreationValidationError(
315
304
  f"Too few question options (got {value})."
@@ -338,7 +327,7 @@ class QuestionOptionsDescriptor(BaseDescriptor):
338
327
  )
339
328
  if not all(
340
329
  [
341
- type(option) != str
330
+ not isinstance(option, str)
342
331
  or (len(option) >= 1 and len(option) < Settings.MAX_OPTION_LENGTH)
343
332
  for option in value
344
333
  ]
@@ -356,12 +345,12 @@ class QuestionOptionsDescriptor(BaseDescriptor):
356
345
  UserWarning,
357
346
  )
358
347
 
359
- if hasattr(instance, "min_selections") and instance.min_selections != None:
348
+ if hasattr(instance, "min_selections") and instance.min_selections is not None:
360
349
  if instance.min_selections > len(value):
361
350
  raise QuestionCreationValidationError(
362
351
  f"You asked for at least {instance.min_selections} selections, but provided fewer options (got {value})."
363
352
  )
364
- if hasattr(instance, "max_selections") and instance.max_selections != None:
353
+ if hasattr(instance, "max_selections") and instance.max_selections is not None:
365
354
  if instance.max_selections > len(value):
366
355
  raise QuestionCreationValidationError(
367
356
  f"You asked for at most {instance.max_selections} selections, but provided fewer options (got {value})."
@@ -396,40 +385,12 @@ class QuestionTextDescriptor(BaseDescriptor):
396
385
  # if len(value) > Settings.MAX_QUESTION_LENGTH:
397
386
  # raise Exception("Question is too long!")
398
387
  if len(value) < 1:
399
- raise Exception("Question is too short!")
388
+
389
+ raise QuestionCreationValidationError("Question is too short!")
400
390
  if not isinstance(value, str):
401
- raise Exception("Question must be a string!")
391
+ raise QuestionCreationValidationError("Question must be a string!")
402
392
 
403
- #value = textwrap.dedent(value).strip()
404
-
405
- # if contains_single_braced_substring(value):
406
- # import warnings
407
-
408
- # # # warnings.warn(
409
- # # # f"WARNING: Question text contains a single-braced substring: If you intended to parameterize the question with a Scenario this should be changed to a double-braced substring, e.g. {{variable}}.\nSee details on constructing Scenarios in the docs: https://docs.expectedparrot.com/en/latest/scenarios.html",
410
- # # # UserWarning,
411
- # # # )
412
- # warnings.warn(
413
- # "WARNING: Question text contains a single-braced substring. "
414
- # "If you intended to parameterize the question with a Scenario, this will "
415
- # "be changed to a double-braced substring, e.g. {{variable}}.\n"
416
- # "See details on constructing Scenarios in the docs: "
417
- # "https://docs.expectedparrot.com/en/latest/scenarios.html",
418
- # UserWarning,
419
- # )
420
- # Automatically replace single braces with double braces
421
- # This is here because if the user is using an f-string, the double brace will get converted to a single brace.
422
- # This undoes that.
423
- # value = re.sub(r"\{([^\{\}]+)\}", r"{{\1}}", value)
424
- return value
425
-
426
- # iterate through all doubles braces and check if they are valid python identifiers
427
- # for match in re.finditer(r"\{\{([^\{\}]+)\}\}", value):
428
- # if " " in match.group(1).strip():
429
- # raise QuestionCreationValidationError(
430
- # f"Question text contains an invalid identifier: '{match.group(1)}'"
431
- # )
432
-
393
+
433
394
  return None
434
395
 
435
396
 
@@ -1,11 +1,19 @@
1
- from typing import Any, SupportsIndex
2
1
  import json
2
+ from typing import Any
3
3
  from pydantic import ValidationError
4
4
 
5
+ from ..base import BaseException
5
6
 
6
- class QuestionErrors(Exception):
7
+ class QuestionErrors(BaseException):
7
8
  """
8
9
  Base exception class for question-related errors.
10
+
11
+ This is the parent class for all exceptions related to question creation,
12
+ validation, processing, and answer handling. It provides a consistent
13
+ interface for error handling across the questions module.
14
+
15
+ Attributes:
16
+ message (str): A human-readable error message explaining the issue
9
17
  """
10
18
 
11
19
  def __init__(self, message="An error occurred with the question"):
@@ -14,19 +22,50 @@ class QuestionErrors(Exception):
14
22
 
15
23
 
16
24
  class QuestionAnswerValidationError(QuestionErrors):
17
- documentation = "https://docs.expectedparrot.com/en/latest/exceptions.html"
25
+ """
26
+ Exception raised when an answer fails validation.
27
+
28
+ This exception occurs when the response from a language model does not
29
+ conform to the expected format or constraints for a specific question type.
30
+
31
+ Common reasons for this exception:
32
+ - Multiple choice: Answer not one of the provided options
33
+ - Numerical: Answer not a valid number or outside allowed range
34
+ - Checkbox: Answer not a valid list of selected options
35
+ - Ranking: Answer does not include all items or has duplicates
36
+
37
+ To fix this error:
38
+ 1. Check the model's response format against the question's requirements
39
+ 2. Verify that the question's instructions are clear for the language model
40
+ 3. Consider using a more capable model if consistent validation failures occur
41
+ 4. Examine the full error details which include the invalid response and validation rules
42
+
43
+ Attributes:
44
+ message (str): The error message
45
+ pydantic_error (ValidationError): Underlying pydantic validation error
46
+ data (dict): The data that failed validation
47
+ model: The pydantic model used for validation
48
+ """
49
+ documentation = "https://docs.expectedparrot.com/en/latest/questions.html#validation"
18
50
 
19
51
  explanation = """
20
- This can occur when the answer coming from the Language Model does not conform to the expectations for the question type.
21
- For example, if the question is a multiple choice question, the answer should be drawn from the list of options provided.
52
+ This error occurs when the answer from the Language Model doesn't match the expected format
53
+ or constraints for the question type. For example:
54
+
55
+ • Multiple choice questions require answers from the provided options
56
+ • Numerical questions need valid numbers within any specified range
57
+ • Checkbox questions require a subset of valid options
58
+ • Matrix questions need responses for each row following column constraints
59
+
60
+ The error details show both what the model returned and the validation rules that were violated.
22
61
  """
23
62
 
24
63
  def __init__(
25
64
  self,
26
- message="Invalid answer.",
27
- pydantic_error: ValidationError = None,
28
- data: dict = None,
29
- model=None,
65
+ message: str,
66
+ data: dict,
67
+ model: Any, # for now
68
+ pydantic_error: ValidationError,
30
69
  ):
31
70
  self.message = message
32
71
  self.pydantic_error = pydantic_error
@@ -45,13 +84,17 @@ class QuestionAnswerValidationError(QuestionErrors):
45
84
  return str(error_list[0].get("msg", "Unknown error"))
46
85
  return str(self.message)
47
86
 
48
- # def __str__(self):
49
- # return f"""{repr(self)}
50
- # Data being validated: {self.data}
51
- # Pydnantic Model: {self.model}.
52
- # Reported error: {self.message}."""
53
-
54
87
  def to_html_dict(self):
88
+ """
89
+ Convert the exception to an HTML-friendly dictionary for rendering.
90
+
91
+ This method is used for creating detailed error reports in HTML format,
92
+ particularly in notebook environments.
93
+
94
+ Returns:
95
+ dict: HTML-formatted error information
96
+ """
97
+ #breakpoint()
55
98
  return {
56
99
  "Exception type": ("p", "/p", self.__class__.__name__),
57
100
  "Explanation": ("p", "/p", self.explanation),
@@ -79,28 +122,241 @@ class QuestionAnswerValidationError(QuestionErrors):
79
122
 
80
123
 
81
124
  class QuestionCreationValidationError(QuestionErrors):
82
- pass
125
+ """
126
+ Exception raised when question creation parameters are invalid.
127
+
128
+ This exception occurs when attempting to create a question with invalid
129
+ parameters, such as:
130
+ - Missing required attributes
131
+ - Invalid option formats
132
+ - Incompatible parameter combinations
133
+
134
+ To fix this error:
135
+ 1. Check the documentation for the specific question type
136
+ 2. Verify all required parameters are provided
137
+ 3. Ensure parameter formats match the question type's expectations
138
+ 4. Confirm that parameter combinations are compatible
139
+
140
+ Examples:
141
+ ```python
142
+ # Missing required options for multiple choice
143
+ MultipleChoice(question_text="Choose one:", options=[]) # Raises QuestionCreationValidationError
144
+
145
+ # Invalid parameter combination
146
+ Numerical(question_text="Enter a value:", min_value=10, max_value=5) # Raises QuestionCreationValidationError
147
+ ```
148
+ """
149
+
150
+ def __init__(self, message="Invalid parameters for question creation"):
151
+ super().__init__(message)
83
152
 
84
153
 
85
154
  class QuestionResponseValidationError(QuestionErrors):
86
- pass
155
+ """
156
+ Exception raised when a response fails structural validation.
157
+
158
+ This exception is similar to QuestionAnswerValidationError but focuses on
159
+ structural validation before content validation. It catches errors in the
160
+ response structure itself.
161
+
162
+ To fix this error:
163
+ 1. Check if the model's response format is correctly structured
164
+ 2. Verify that the response contains all required fields
165
+ 3. Ensure the response data types match expectations
166
+
167
+ Note: This exception is primarily used in tests and internal validation.
168
+ In most cases, QuestionAnswerValidationError provides more detailed information.
169
+ """
170
+
171
+ def __init__(self, message="The response structure is invalid"):
172
+ super().__init__(message)
87
173
 
88
174
 
89
175
  class QuestionAttributeMissing(QuestionErrors):
90
- pass
176
+ """
177
+ Exception raised when a required question attribute is missing.
178
+
179
+ This exception occurs when attempting to use a question that is missing
180
+ essential attributes needed for its operation.
181
+
182
+ To fix this error:
183
+ 1. Check that the question has been properly initialized
184
+ 2. Verify all required attributes are set
185
+ 3. Ensure any parent class initialization is called correctly
186
+
187
+ Note: While defined in the codebase, this exception is not actively raised
188
+ and may be used for future validation enhancements.
189
+ """
190
+
191
+ def __init__(self, message="A required question attribute is missing"):
192
+ super().__init__(message)
91
193
 
92
194
 
93
195
  class QuestionSerializationError(QuestionErrors):
94
- pass
196
+ """
197
+ Exception raised when question serialization or deserialization fails.
198
+
199
+ This exception occurs when:
200
+ - A question cannot be properly converted to JSON format
201
+ - A serialized question cannot be reconstructed from its JSON representation
202
+ - Required fields are missing in the serialized data
203
+
204
+ To fix this error:
205
+ 1. Ensure the question and all its attributes are serializable
206
+ 2. When deserializing, verify the data format matches what's expected
207
+ 3. Check for version compatibility if deserializing from an older version
208
+
209
+ Examples:
210
+ ```python
211
+ question.to_dict() # Raises QuestionSerializationError if contains unserializable attributes
212
+ ```
213
+ """
214
+
215
+ def __init__(self, message="Failed to serialize or deserialize question"):
216
+ super().__init__(message)
95
217
 
96
218
 
97
219
  class QuestionScenarioRenderError(QuestionErrors):
98
- pass
220
+ """
221
+ Exception raised when a scenario cannot be rendered for a question.
222
+
223
+ This exception occurs when:
224
+ - The scenario template has syntax errors
225
+ - Required variables for the template are missing
226
+ - The rendered scenario exceeds size limits
227
+
228
+ To fix this error:
229
+ 1. Check the scenario template syntax
230
+ 2. Ensure all required variables are provided to the template
231
+ 3. Verify that the scenario size is within acceptable limits
232
+
233
+ Examples:
234
+ ```python
235
+ question.with_scenario(scenario_with_invalid_template) # Raises QuestionScenarioRenderError
236
+ ```
237
+ """
238
+
239
+ def __init__(self, message="Failed to render scenario for question"):
240
+ super().__init__(message)
99
241
 
100
242
 
101
243
  class QuestionMissingTypeError(QuestionErrors):
102
- pass
244
+ """
245
+ Exception raised when a question class is missing a required type attribute.
246
+
247
+ This exception occurs during question registration when a question class
248
+ doesn't define the required question_type attribute. All question classes
249
+ must have this attribute for proper registration and identification.
250
+
251
+ To fix this error:
252
+ 1. Add the question_type class attribute to the question class
253
+ 2. Ensure the question_type is a unique identifier for the question type
254
+
255
+ Examples:
256
+ ```python
257
+ class MyQuestion(Question): # Missing question_type attribute
258
+ pass
259
+ # Registration would raise QuestionMissingTypeError
260
+ ```
261
+ """
262
+
263
+ def __init__(self, message="Question class is missing required type attribute"):
264
+ super().__init__(message)
103
265
 
104
266
 
105
267
  class QuestionBadTypeError(QuestionErrors):
106
- pass
268
+ """
269
+ Exception raised when a question class has an invalid __init__ method signature.
270
+
271
+ This exception occurs during question registration when a question class
272
+ doesn't have the required parameters in its __init__ method. All question
273
+ classes must follow a standard parameter pattern for consistency.
274
+
275
+ To fix this error:
276
+ 1. Ensure the question class __init__ method includes the necessary parameters
277
+ 2. Match the parameter pattern required by the question registry
278
+
279
+ Examples:
280
+ ```python
281
+ class MyQuestion(Question):
282
+ question_type = "my_question"
283
+ def __init__(self, missing_required_params): # Invalid signature
284
+ pass
285
+ # Registration would raise QuestionBadTypeError
286
+ ```
287
+ """
288
+
289
+ def __init__(self, message="Question class has invalid __init__ method signature"):
290
+ super().__init__(message)
291
+
292
+
293
+ class QuestionTypeError(QuestionErrors):
294
+ """
295
+ Exception raised when a TypeError occurs in the questions module.
296
+
297
+ This exception wraps standard Python TypeErrors to provide a consistent
298
+ exception handling approach within the EDSL framework. It's used when
299
+ a type-related error occurs during question operations.
300
+
301
+ Examples:
302
+ - Attempting to access or operate on a question attribute with the wrong type
303
+ - Passing incorrect types to question methods
304
+ - Type conversion failures during question processing
305
+ """
306
+
307
+ def __init__(self, message="A type error occurred while processing the question"):
308
+ super().__init__(message)
309
+
310
+
311
+ class QuestionValueError(QuestionErrors):
312
+ """
313
+ Exception raised when a ValueError occurs in the questions module.
314
+
315
+ This exception wraps standard Python ValueErrors to provide a consistent
316
+ exception handling approach within the EDSL framework. It's used when
317
+ a value-related error occurs during question operations.
318
+
319
+ Examples:
320
+ - Invalid values for question parameters
321
+ - Out-of-range values for numerical questions
322
+ - Invalid option selections for multiple choice questions
323
+ """
324
+
325
+ def __init__(self, message="An invalid value was provided for the question"):
326
+ super().__init__(message)
327
+
328
+
329
+ class QuestionKeyError(QuestionErrors):
330
+ """
331
+ Exception raised when a KeyError occurs in the questions module.
332
+
333
+ This exception wraps standard Python KeyErrors to provide a consistent
334
+ exception handling approach within the EDSL framework. It's used when
335
+ a key-related error occurs during question operations.
336
+
337
+ Examples:
338
+ - Attempting to access a non-existent attribute via dictionary-style access
339
+ - Missing keys in question option dictionaries
340
+ - Key errors during question serialization or deserialization
341
+ """
342
+
343
+ def __init__(self, message="A key error occurred while processing the question"):
344
+ super().__init__(message)
345
+
346
+
347
+ class QuestionNotImplementedError(QuestionErrors):
348
+ """
349
+ Exception raised when a method that should be implemented is not.
350
+
351
+ This exception wraps standard Python NotImplementedError to provide a consistent
352
+ exception handling approach within the EDSL framework. It's used when
353
+ a required method is called but not implemented.
354
+
355
+ Examples:
356
+ - Abstract methods that must be overridden in subclasses
357
+ - Placeholder methods that should be implemented in concrete classes
358
+ - Methods that are required by an interface but not yet implemented
359
+ """
360
+
361
+ def __init__(self, message="This method must be implemented in a subclass"):
362
+ super().__init__(message)
@@ -1,4 +1,4 @@
1
- from typing import List, Any, Dict, Union
1
+ from typing import List, Any, Dict
2
2
  from jinja2 import Environment, Undefined
3
3
  from .question_base import QuestionBase
4
4
  from ..scenarios import ScenarioList
@@ -68,7 +68,6 @@ class LoopProcessor:
68
68
  return value
69
69
 
70
70
  if key == "option_labels":
71
- import json
72
71
 
73
72
  return (
74
73
  eval(self._render_template(value, scenario))
@@ -88,7 +87,8 @@ class LoopProcessor:
88
87
  if isinstance(value, (int, float)):
89
88
  return value
90
89
 
91
- raise ValueError(f"Unexpected value type: {type(value)} for key '{key}'")
90
+ from .exceptions import QuestionValueError
91
+ raise QuestionValueError(f"Unexpected value type: {type(value)} for key '{key}'")
92
92
 
93
93
  def _render_template(self, template: str, scenario: Dict[str, Any]) -> str:
94
94
  """Render a single template string.
@@ -130,8 +130,10 @@ class LoopProcessor:
130
130
 
131
131
  def replace_var(match):
132
132
  var_name = match.group('var')
133
- open_brace = match.group('open')
134
- close_brace = match.group('close')
133
+ # We're keeping the original formatting with braces
134
+ # but not using these variables directly
135
+ # open_brace = match.group('open')
136
+ # close_brace = match.group('close')
135
137
 
136
138
  # Try to evaluate the variable in the context
137
139
  try:
@@ -12,6 +12,9 @@ Your response should be only a valid JSON in the following format:
12
12
  }
13
13
  {% endif %}
14
14
 
15
+ {% if min_list_items is not none %}
16
+ The list must contain at least {{ min_list_items }} items.
17
+ {% endif %}
15
18
  {% if max_list_items is not none %}
16
19
  The list must not contain more than {{ max_list_items }} items.
17
20
  {% endif %}
@@ -48,25 +48,27 @@ Technical Details:
48
48
 
49
49
  from __future__ import annotations
50
50
  from abc import ABC
51
- from typing import Any, Type, Optional, List, Callable, Union, TypedDict, TYPE_CHECKING
51
+ from typing import Any, Type, Optional, Union, TypedDict, TYPE_CHECKING, Literal
52
52
 
53
+ from .descriptors import QuestionNameDescriptor, QuestionTextDescriptor
54
+ from .answer_validator_mixin import AnswerValidatorMixin
55
+ from .register_questions_meta import RegisterQuestionsMeta
56
+ from .simple_ask_mixin import SimpleAskMixin
57
+ from .question_base_prompts_mixin import QuestionBasePromptsMixin
58
+ from .question_base_gen_mixin import QuestionBaseGenMixin
53
59
  from .exceptions import QuestionSerializationError
54
60
 
55
61
  from ..base import PersistenceMixin, RepresentationMixin, BaseDiff, BaseDiffCollection
56
62
  from ..utilities import remove_edsl_version, is_valid_variable_name
57
63
 
64
+ # Define VisibilityType for type annotations
65
+ VisibilityType = Literal["private", "public", "unlisted"]
66
+
58
67
  if TYPE_CHECKING:
59
68
  from ..agents import Agent
60
69
  from ..scenarios import Scenario
61
70
  from ..surveys import Survey
62
71
 
63
- from .descriptors import QuestionNameDescriptor, QuestionTextDescriptor
64
- from .answer_validator_mixin import AnswerValidatorMixin
65
- from .register_questions_meta import RegisterQuestionsMeta
66
- from .simple_ask_mixin import SimpleAskMixin
67
- from .question_base_prompts_mixin import QuestionBasePromptsMixin
68
- from .question_base_gen_mixin import QuestionBaseGenMixin
69
-
70
72
  if TYPE_CHECKING:
71
73
  from .response_validator_abc import ResponseValidatorABC
72
74
  from ..language_models import LanguageModel
@@ -202,11 +204,6 @@ class QuestionBase(
202
204
  >>> q = QuestionFreeText(question_name="valid_name", question_text="Text")
203
205
  >>> q.is_valid_question_name()
204
206
  True
205
-
206
- >>> q = QuestionFreeText(question_name="123invalid", question_text="Text")
207
- Traceback (most recent call last):
208
- ...
209
- edsl.questions.exceptions.QuestionCreationValidationError: `question_name` is not a valid variable name (got 123invalid).
210
207
  """
211
208
  return is_valid_variable_name(self.question_name)
212
209
 
@@ -509,9 +506,9 @@ class QuestionBase(
509
506
  int(key): value for key, value in options_labels.items()
510
507
  }
511
508
  local_data["option_labels"] = options_labels
512
- except:
509
+ except Exception as e:
513
510
  raise QuestionSerializationError(
514
- f"Data does not have a 'question_type' field (got {data})."
511
+ f"Error in deserialization: {str(e)}. Data does not have a 'question_type' field (got {data})."
515
512
  )
516
513
  from .question_registry import get_question_class
517
514
 
@@ -697,7 +694,8 @@ class QuestionBase(
697
694
  try:
698
695
  return getattr(self, key)
699
696
  except TypeError:
700
- raise KeyError(f"Question has no attribute {key} of type {type(key)}")
697
+ from .exceptions import QuestionKeyError
698
+ raise QuestionKeyError(f"Question has no attribute {key} of type {type(key)}")
701
699
 
702
700
  def __repr__(self) -> str:
703
701
  """Return a string representation of the question. Should be able to be used to reconstruct the question.
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
  import copy
3
3
  import itertools
4
- from typing import Optional, List, Callable, Type, TYPE_CHECKING, Union
4
+ from typing import Optional, List, Callable, TYPE_CHECKING, Union
5
5
 
6
6
  if TYPE_CHECKING:
7
7
  from .question_base import QuestionBase
@@ -174,7 +174,7 @@ class QuestionBaseGenMixin:
174
174
  return result
175
175
  except self.MaxTemplateNestingExceeded:
176
176
  raise
177
- except Exception as e:
177
+ except Exception:
178
178
  import warnings
179
179
  warnings.warn("Failed to render string: " + value)
180
180
  return value
@@ -1,9 +1,14 @@
1
1
  from importlib import resources
2
- from typing import Optional
2
+ from typing import Optional, TYPE_CHECKING
3
3
  from functools import lru_cache
4
4
 
5
5
  from .exceptions import QuestionAnswerValidationError
6
6
 
7
+ if TYPE_CHECKING:
8
+ from pydantic import BaseModel
9
+ from ..prompts import Prompt
10
+ from ..prompts.prompt import PromptBase
11
+
7
12
  class TemplateManager:
8
13
  _instance = None
9
14
 
@@ -85,7 +90,7 @@ class QuestionBasePromptsMixin:
85
90
 
86
91
  @classmethod
87
92
  def path_to_folder(cls) -> str:
88
- return resources.files(f"edsl.questions.templates", cls.question_type)
93
+ return resources.files("edsl.questions.templates", cls.question_type)
89
94
 
90
95
  @property
91
96
  def response_model(self) -> type["BaseModel"]:
@@ -179,7 +184,8 @@ class QuestionBasePromptsMixin:
179
184
  except QuestionAnswerValidationError:
180
185
  pass
181
186
  else:
182
- raise ValueError(f"Example {answer} should have failed for {reason}.")
187
+ from .exceptions import QuestionValueError
188
+ raise QuestionValueError(f"Example {answer} should have failed for {reason}.")
183
189
 
184
190
  @property
185
191
  def new_default_instructions(self) -> "Prompt":