edsl 0.1.49__py3-none-any.whl → 0.1.51__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 (257) 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 +107 -0
  14. edsl/buckets/model_buckets.py +1 -2
  15. edsl/buckets/token_bucket.py +11 -6
  16. edsl/buckets/token_bucket_api.py +27 -12
  17. edsl/buckets/token_bucket_client.py +9 -7
  18. edsl/caching/cache.py +12 -4
  19. edsl/caching/cache_entry.py +10 -9
  20. edsl/caching/exceptions.py +113 -7
  21. edsl/caching/remote_cache_sync.py +6 -7
  22. edsl/caching/sql_dict.py +20 -14
  23. edsl/cli.py +43 -0
  24. edsl/config/__init__.py +1 -1
  25. edsl/config/config_class.py +32 -6
  26. edsl/conversation/Conversation.py +8 -4
  27. edsl/conversation/car_buying.py +1 -3
  28. edsl/conversation/exceptions.py +58 -0
  29. edsl/conversation/mug_negotiation.py +2 -8
  30. edsl/coop/__init__.py +28 -6
  31. edsl/coop/coop.py +120 -29
  32. edsl/coop/coop_functions.py +1 -1
  33. edsl/coop/ep_key_handling.py +1 -1
  34. edsl/coop/exceptions.py +188 -9
  35. edsl/coop/price_fetcher.py +5 -8
  36. edsl/coop/utils.py +4 -6
  37. edsl/dataset/__init__.py +5 -4
  38. edsl/dataset/dataset.py +177 -86
  39. edsl/dataset/dataset_operations_mixin.py +98 -76
  40. edsl/dataset/dataset_tree.py +11 -7
  41. edsl/dataset/display/table_display.py +0 -2
  42. edsl/dataset/display/table_renderers.py +6 -4
  43. edsl/dataset/exceptions.py +125 -0
  44. edsl/dataset/file_exports.py +18 -11
  45. edsl/dataset/r/ggplot.py +13 -6
  46. edsl/display/__init__.py +27 -0
  47. edsl/display/core.py +147 -0
  48. edsl/display/plugin.py +189 -0
  49. edsl/display/utils.py +52 -0
  50. edsl/inference_services/__init__.py +9 -1
  51. edsl/inference_services/available_model_cache_handler.py +1 -1
  52. edsl/inference_services/available_model_fetcher.py +5 -6
  53. edsl/inference_services/data_structures.py +10 -7
  54. edsl/inference_services/exceptions.py +132 -1
  55. edsl/inference_services/inference_service_abc.py +2 -2
  56. edsl/inference_services/inference_services_collection.py +2 -6
  57. edsl/inference_services/registry.py +4 -3
  58. edsl/inference_services/service_availability.py +4 -3
  59. edsl/inference_services/services/anthropic_service.py +4 -1
  60. edsl/inference_services/services/aws_bedrock.py +13 -12
  61. edsl/inference_services/services/azure_ai.py +12 -10
  62. edsl/inference_services/services/deep_infra_service.py +1 -4
  63. edsl/inference_services/services/deep_seek_service.py +1 -5
  64. edsl/inference_services/services/google_service.py +7 -3
  65. edsl/inference_services/services/groq_service.py +1 -1
  66. edsl/inference_services/services/mistral_ai_service.py +4 -2
  67. edsl/inference_services/services/ollama_service.py +1 -1
  68. edsl/inference_services/services/open_ai_service.py +7 -5
  69. edsl/inference_services/services/perplexity_service.py +6 -2
  70. edsl/inference_services/services/test_service.py +8 -7
  71. edsl/inference_services/services/together_ai_service.py +2 -3
  72. edsl/inference_services/services/xai_service.py +1 -1
  73. edsl/instructions/__init__.py +1 -1
  74. edsl/instructions/change_instruction.py +7 -5
  75. edsl/instructions/exceptions.py +61 -0
  76. edsl/instructions/instruction.py +6 -2
  77. edsl/instructions/instruction_collection.py +6 -4
  78. edsl/instructions/instruction_handler.py +12 -15
  79. edsl/interviews/ReportErrors.py +0 -3
  80. edsl/interviews/__init__.py +9 -2
  81. edsl/interviews/answering_function.py +11 -13
  82. edsl/interviews/exception_tracking.py +15 -8
  83. edsl/interviews/exceptions.py +79 -0
  84. edsl/interviews/interview.py +33 -30
  85. edsl/interviews/interview_status_dictionary.py +4 -2
  86. edsl/interviews/interview_status_log.py +2 -1
  87. edsl/interviews/interview_task_manager.py +5 -5
  88. edsl/interviews/request_token_estimator.py +5 -2
  89. edsl/interviews/statistics.py +3 -4
  90. edsl/invigilators/__init__.py +7 -1
  91. edsl/invigilators/exceptions.py +79 -0
  92. edsl/invigilators/invigilator_base.py +0 -1
  93. edsl/invigilators/invigilators.py +9 -13
  94. edsl/invigilators/prompt_constructor.py +1 -5
  95. edsl/invigilators/prompt_helpers.py +8 -4
  96. edsl/invigilators/question_instructions_prompt_builder.py +1 -1
  97. edsl/invigilators/question_option_processor.py +9 -5
  98. edsl/invigilators/question_template_replacements_builder.py +3 -2
  99. edsl/jobs/__init__.py +42 -5
  100. edsl/jobs/async_interview_runner.py +25 -23
  101. edsl/jobs/check_survey_scenario_compatibility.py +11 -10
  102. edsl/jobs/data_structures.py +8 -5
  103. edsl/jobs/exceptions.py +177 -8
  104. edsl/jobs/fetch_invigilator.py +1 -1
  105. edsl/jobs/jobs.py +74 -69
  106. edsl/jobs/jobs_checks.py +6 -7
  107. edsl/jobs/jobs_component_constructor.py +4 -4
  108. edsl/jobs/jobs_pricing_estimation.py +4 -3
  109. edsl/jobs/jobs_remote_inference_logger.py +5 -4
  110. edsl/jobs/jobs_runner_asyncio.py +3 -4
  111. edsl/jobs/jobs_runner_status.py +8 -9
  112. edsl/jobs/remote_inference.py +27 -24
  113. edsl/jobs/results_exceptions_handler.py +10 -7
  114. edsl/key_management/__init__.py +3 -1
  115. edsl/key_management/exceptions.py +62 -0
  116. edsl/key_management/key_lookup.py +1 -1
  117. edsl/key_management/key_lookup_builder.py +37 -14
  118. edsl/key_management/key_lookup_collection.py +2 -0
  119. edsl/language_models/__init__.py +1 -1
  120. edsl/language_models/exceptions.py +302 -14
  121. edsl/language_models/language_model.py +9 -8
  122. edsl/language_models/model.py +4 -4
  123. edsl/language_models/model_list.py +1 -1
  124. edsl/language_models/price_manager.py +1 -1
  125. edsl/language_models/raw_response_handler.py +14 -9
  126. edsl/language_models/registry.py +17 -21
  127. edsl/language_models/repair.py +0 -6
  128. edsl/language_models/unused/fake_openai_service.py +0 -1
  129. edsl/load_plugins.py +69 -0
  130. edsl/logger.py +146 -0
  131. edsl/notebooks/__init__.py +24 -1
  132. edsl/notebooks/exceptions.py +82 -0
  133. edsl/notebooks/notebook.py +7 -3
  134. edsl/notebooks/notebook_to_latex.py +1 -2
  135. edsl/plugins/__init__.py +63 -0
  136. edsl/plugins/built_in/export_example.py +50 -0
  137. edsl/plugins/built_in/pig_latin.py +67 -0
  138. edsl/plugins/cli.py +372 -0
  139. edsl/plugins/cli_typer.py +283 -0
  140. edsl/plugins/exceptions.py +31 -0
  141. edsl/plugins/hookspec.py +51 -0
  142. edsl/plugins/plugin_host.py +128 -0
  143. edsl/plugins/plugin_manager.py +633 -0
  144. edsl/plugins/plugins_registry.py +168 -0
  145. edsl/prompts/__init__.py +24 -1
  146. edsl/prompts/exceptions.py +107 -5
  147. edsl/prompts/prompt.py +15 -7
  148. edsl/questions/HTMLQuestion.py +5 -11
  149. edsl/questions/Quick.py +0 -1
  150. edsl/questions/__init__.py +6 -4
  151. edsl/questions/answer_validator_mixin.py +318 -323
  152. edsl/questions/compose_questions.py +3 -3
  153. edsl/questions/descriptors.py +11 -50
  154. edsl/questions/exceptions.py +278 -22
  155. edsl/questions/loop_processor.py +7 -5
  156. edsl/questions/prompt_templates/question_list.jinja +3 -0
  157. edsl/questions/question_base.py +46 -19
  158. edsl/questions/question_base_gen_mixin.py +2 -2
  159. edsl/questions/question_base_prompts_mixin.py +13 -7
  160. edsl/questions/question_budget.py +503 -98
  161. edsl/questions/question_check_box.py +660 -160
  162. edsl/questions/question_dict.py +345 -194
  163. edsl/questions/question_extract.py +401 -61
  164. edsl/questions/question_free_text.py +80 -14
  165. edsl/questions/question_functional.py +119 -9
  166. edsl/questions/{derived/question_likert_five.py → question_likert_five.py} +2 -2
  167. edsl/questions/{derived/question_linear_scale.py → question_linear_scale.py} +3 -4
  168. edsl/questions/question_list.py +275 -28
  169. edsl/questions/question_matrix.py +643 -96
  170. edsl/questions/question_multiple_choice.py +219 -51
  171. edsl/questions/question_numerical.py +361 -32
  172. edsl/questions/question_rank.py +401 -124
  173. edsl/questions/question_registry.py +7 -5
  174. edsl/questions/{derived/question_top_k.py → question_top_k.py} +3 -3
  175. edsl/questions/{derived/question_yes_no.py → question_yes_no.py} +3 -4
  176. edsl/questions/register_questions_meta.py +2 -2
  177. edsl/questions/response_validator_abc.py +13 -15
  178. edsl/questions/response_validator_factory.py +10 -12
  179. edsl/questions/templates/dict/answering_instructions.jinja +1 -0
  180. edsl/questions/templates/rank/question_presentation.jinja +1 -1
  181. edsl/results/__init__.py +1 -1
  182. edsl/results/exceptions.py +141 -7
  183. edsl/results/report.py +1 -2
  184. edsl/results/result.py +11 -9
  185. edsl/results/results.py +480 -321
  186. edsl/results/results_selector.py +8 -4
  187. edsl/scenarios/PdfExtractor.py +2 -2
  188. edsl/scenarios/construct_download_link.py +69 -35
  189. edsl/scenarios/directory_scanner.py +33 -14
  190. edsl/scenarios/document_chunker.py +1 -1
  191. edsl/scenarios/exceptions.py +238 -14
  192. edsl/scenarios/file_methods.py +1 -1
  193. edsl/scenarios/file_store.py +7 -3
  194. edsl/scenarios/handlers/__init__.py +17 -0
  195. edsl/scenarios/handlers/docx_file_store.py +0 -5
  196. edsl/scenarios/handlers/pdf_file_store.py +0 -1
  197. edsl/scenarios/handlers/pptx_file_store.py +0 -5
  198. edsl/scenarios/handlers/py_file_store.py +0 -1
  199. edsl/scenarios/handlers/sql_file_store.py +1 -4
  200. edsl/scenarios/handlers/sqlite_file_store.py +0 -1
  201. edsl/scenarios/handlers/txt_file_store.py +1 -1
  202. edsl/scenarios/scenario.py +1 -3
  203. edsl/scenarios/scenario_list.py +179 -27
  204. edsl/scenarios/scenario_list_pdf_tools.py +1 -0
  205. edsl/scenarios/scenario_selector.py +0 -1
  206. edsl/surveys/__init__.py +3 -4
  207. edsl/surveys/dag/__init__.py +4 -2
  208. edsl/surveys/descriptors.py +1 -1
  209. edsl/surveys/edit_survey.py +1 -0
  210. edsl/surveys/exceptions.py +165 -9
  211. edsl/surveys/memory/__init__.py +5 -3
  212. edsl/surveys/memory/memory_management.py +1 -0
  213. edsl/surveys/memory/memory_plan.py +6 -15
  214. edsl/surveys/rules/__init__.py +5 -3
  215. edsl/surveys/rules/rule.py +1 -2
  216. edsl/surveys/rules/rule_collection.py +1 -1
  217. edsl/surveys/survey.py +12 -24
  218. edsl/surveys/survey_css.py +3 -3
  219. edsl/surveys/survey_export.py +6 -3
  220. edsl/surveys/survey_flow_visualization.py +10 -1
  221. edsl/surveys/survey_simulator.py +2 -1
  222. edsl/tasks/__init__.py +23 -1
  223. edsl/tasks/exceptions.py +72 -0
  224. edsl/tasks/question_task_creator.py +3 -3
  225. edsl/tasks/task_creators.py +1 -3
  226. edsl/tasks/task_history.py +8 -10
  227. edsl/tasks/task_status_log.py +1 -2
  228. edsl/tokens/__init__.py +29 -1
  229. edsl/tokens/exceptions.py +37 -0
  230. edsl/tokens/interview_token_usage.py +3 -2
  231. edsl/tokens/token_usage.py +4 -3
  232. edsl/utilities/__init__.py +21 -1
  233. edsl/utilities/decorators.py +1 -2
  234. edsl/utilities/markdown_to_docx.py +2 -2
  235. edsl/utilities/markdown_to_pdf.py +1 -1
  236. edsl/utilities/repair_functions.py +0 -1
  237. edsl/utilities/restricted_python.py +0 -1
  238. edsl/utilities/template_loader.py +2 -3
  239. edsl/utilities/utilities.py +8 -29
  240. {edsl-0.1.49.dist-info → edsl-0.1.51.dist-info}/METADATA +32 -2
  241. edsl-0.1.51.dist-info/RECORD +365 -0
  242. edsl-0.1.51.dist-info/entry_points.txt +3 -0
  243. edsl/dataset/smart_objects.py +0 -96
  244. edsl/exceptions/BaseException.py +0 -21
  245. edsl/exceptions/__init__.py +0 -54
  246. edsl/exceptions/configuration.py +0 -16
  247. edsl/exceptions/general.py +0 -34
  248. edsl/questions/derived/__init__.py +0 -0
  249. edsl/study/ObjectEntry.py +0 -173
  250. edsl/study/ProofOfWork.py +0 -113
  251. edsl/study/SnapShot.py +0 -80
  252. edsl/study/Study.py +0 -520
  253. edsl/study/__init__.py +0 -6
  254. edsl/utilities/interface.py +0 -135
  255. edsl-0.1.49.dist-info/RECORD +0 -347
  256. {edsl-0.1.49.dist-info → edsl-0.1.51.dist-info}/LICENSE +0 -0
  257. {edsl-0.1.49.dist-info → edsl-0.1.51.dist-info}/WHEEL +0 -0
@@ -4,12 +4,34 @@ from typing import Union, Literal, Optional, List, Any
4
4
  from jinja2 import Template
5
5
  from pydantic import BaseModel, Field
6
6
 
7
- from ..scenarios import Scenario
8
7
  from .question_base import QuestionBase
9
8
  from .descriptors import QuestionOptionsDescriptor
10
9
  from .decorators import inject_exception
11
10
  from .response_validator_abc import ResponseValidatorABC
12
11
 
12
+ class BaseMultipleChoiceResponse(BaseModel):
13
+ """
14
+ Base model for multiple choice responses.
15
+
16
+ Attributes:
17
+ answer: The selected choice
18
+ comment: Optional comment field
19
+ generated_tokens: Optional raw tokens generated by the model
20
+ """
21
+ answer: Any = Field(..., description="Selected choice")
22
+ comment: Optional[str] = Field(None, description="Optional comment field")
23
+ generated_tokens: Optional[Any] = Field(None, description="Generated tokens")
24
+
25
+ """
26
+ Examples:
27
+ >>> model = BaseMultipleChoiceResponse(answer="Option A", comment="My reasoning")
28
+ >>> model.answer
29
+ 'Option A'
30
+ >>> model.comment
31
+ 'My reasoning'
32
+ """
33
+
34
+
13
35
  def create_response_model(choices: List[str], permissive: bool = False):
14
36
  """
15
37
  Create a ChoiceResponse model class with a predefined list of choices.
@@ -17,95 +39,238 @@ def create_response_model(choices: List[str], permissive: bool = False):
17
39
  :param choices: A list of allowed values for the answer field.
18
40
  :param permissive: If True, any value will be accepted as an answer.
19
41
  :return: A new Pydantic model class.
42
+
43
+ Examples:
44
+ >>> model = create_response_model(["Red", "Green", "Blue"], permissive=False)
45
+ >>> response = model(answer="Red")
46
+ >>> response.answer
47
+ 'Red'
48
+
49
+ >>> try:
50
+ ... model(answer="Purple")
51
+ ... except Exception:
52
+ ... print("Invalid value")
53
+ Invalid value
54
+
55
+ >>> permissive_model = create_response_model(["Red", "Green", "Blue"], permissive=True)
56
+ >>> response = permissive_model(answer="Purple")
57
+ >>> response.answer
58
+ 'Purple'
20
59
  """
21
60
  choice_tuple = tuple(choices)
22
61
 
23
62
  if not permissive:
24
-
25
- class ChoiceResponse(BaseModel):
63
+ class ChoiceResponse(BaseMultipleChoiceResponse):
64
+ """
65
+ A model for multiple choice responses with strict validation.
66
+
67
+ Attributes:
68
+ answer: Must be one of the predefined choices
69
+
70
+ Examples:
71
+ >>> choices = ["Option A", "Option B", "Option C"]
72
+ >>> model = create_response_model(choices, permissive=False)
73
+ >>> response = model(answer="Option A")
74
+ >>> response.answer
75
+ 'Option A'
76
+ """
26
77
  answer: Literal[choice_tuple] = Field(description="Selected choice")
27
- comment: Optional[str] = Field(None, description="Optional comment field")
28
- generated_tokens: Optional[Any] = Field(
29
- None, description="Generated tokens"
30
- )
31
-
32
- class Config:
33
- @staticmethod
34
- def json_schema_extra(schema: dict, model: BaseModel) -> None:
35
- for prop in schema.get("properties", {}).values():
36
- if prop.get("title") == "answer":
37
- prop["enum"] = choices
38
78
 
79
+ model_config = {
80
+ "json_schema_extra": {
81
+ "properties": {
82
+ "answer": {
83
+ "enum": choices
84
+ }
85
+ }
86
+ }
87
+ }
39
88
  else:
89
+ class ChoiceResponse(BaseMultipleChoiceResponse):
90
+ """
91
+ A model for multiple choice responses with permissive validation.
92
+
93
+ Attributes:
94
+ answer: Can be any value, with suggested choices provided
95
+
96
+ Examples:
97
+ >>> choices = ["Option A", "Option B", "Option C"]
98
+ >>> model = create_response_model(choices, permissive=True)
99
+ >>> response = model(answer="Something Else")
100
+ >>> response.answer
101
+ 'Something Else'
102
+ """
103
+ answer: Any = Field(description=f"Selected choice (can be any value). Suggested choices are: {choices}")
40
104
 
41
- class ChoiceResponse(BaseModel):
42
- answer: Any = Field(description="Selected choice (can be any value)")
43
- comment: Optional[str] = Field(None, description="Optional comment field")
44
- generated_tokens: Optional[Any] = Field(
45
- None, description="Generated tokens"
46
- )
47
-
48
- class Config:
49
- @staticmethod
50
- def json_schema_extra(schema: dict, model: BaseModel) -> None:
51
- for prop in schema.get("properties", {}).values():
52
- if prop.get("title") == "answer":
53
- prop["description"] += f". Suggested choices are: {choices}"
54
- schema["title"] += " (Permissive)"
105
+ model_config = {
106
+ "title": "PermissiveChoiceResponse"
107
+ }
55
108
 
56
109
  return ChoiceResponse
57
110
 
58
111
 
59
112
  class MultipleChoiceResponseValidator(ResponseValidatorABC):
113
+ """
114
+ Validator for multiple choice responses.
115
+
116
+ This validator ensures that the answer is one of the allowed options.
117
+ In permissive mode, any answer is accepted.
118
+
119
+ Examples:
120
+ >>> from edsl.questions import QuestionMultipleChoice
121
+ >>> q = QuestionMultipleChoice(
122
+ ... question_name="feeling",
123
+ ... question_text="How are you feeling?",
124
+ ... question_options=["Good", "Bad", "Neutral"]
125
+ ... )
126
+ >>> validator = q.response_validator
127
+ >>> result = validator.validate({"answer": "Good"})
128
+ >>> sorted(result.keys())
129
+ ['answer', 'comment', 'generated_tokens']
130
+ >>> result["answer"]
131
+ 'Good'
132
+ """
60
133
  required_params = ["question_options", "use_code"]
61
134
 
62
135
  def fix(self, response, verbose=False):
63
- response_text = str(response.get("answer"))
64
- if response_text is None:
65
- response_text = response.get("generated_tokens", "")
136
+ """
137
+ Attempt to fix an invalid multiple choice response.
138
+
139
+ Strategies:
140
+ 1. Extract an option mentioned in the generated text
141
+ 2. Check for exact matches in the text
142
+ 3. Look for substring matches
143
+ 4. Normalize whitespace and check for matches
144
+ 5. Check if the answer is a prefix of any option (ignoring trailing spaces/punctuation)
145
+
146
+ Parameters:
147
+ response: The invalid response to fix
148
+ verbose: Whether to print debug information
149
+
150
+ Returns:
151
+ A fixed response dict if possible, otherwise the original response
152
+
153
+ Examples:
154
+ >>> from edsl.questions import QuestionMultipleChoice
155
+ >>> q = QuestionMultipleChoice.example()
156
+ >>> validator = q.response_validator
157
+ >>> result = validator.fix({"answer": "I'm feeling Good today"})
158
+ >>> sorted(result.keys())
159
+ ['answer', 'comment', 'generated_tokens']
160
+ >>> result["answer"]
161
+ 'Good'
162
+ """
163
+ # Don't attempt to fix None values - they should be properly rejected
164
+ if response.get("answer") is None:
165
+ if verbose:
166
+ print("Not attempting to fix None answer value")
167
+ return response
168
+
169
+ # Get the raw text to analyze
170
+ response_text = str(response.get("answer", ""))
171
+ if not response_text:
172
+ response_text = str(response.get("generated_tokens", ""))
66
173
 
67
174
  if verbose:
68
- print(f"Invalid generated tokens was: {response_text}")
175
+ print(f"Invalid response text: {response_text}")
176
+ print(f"Looking for options among: {self.question_options}")
69
177
 
178
+ # Strategy 1: Look for exact options in the text
70
179
  matches = []
71
- for idx, option in enumerate(self.question_options):
72
- if verbose:
73
- print("The options are: ", self.question_options)
74
- if str(option) in response_text:
180
+ for option in self.question_options:
181
+ option_str = str(option)
182
+ if option_str in response_text:
75
183
  if verbose:
76
- print("Match found with option ", option)
184
+ print(f"Match found with option: {option_str}")
77
185
  if option not in matches:
78
186
  matches.append(option)
79
187
 
80
- if verbose:
81
- print("The matches are: ", matches)
188
+ # If we have exactly one match, use it
82
189
  if len(matches) == 1:
190
+ fixed_answer = matches[0]
83
191
  proposed_data = {
84
- "answer": matches[0],
85
- "generated_tokens": response.get("generated_tokens", None),
192
+ "answer": fixed_answer,
193
+ "comment": response.get("comment"),
194
+ "generated_tokens": response.get("generated_tokens"),
86
195
  }
196
+
87
197
  try:
88
- self.response_model(**proposed_data)
198
+ # Validate the fixed answer
199
+ self.response_model.model_validate(proposed_data)
200
+ if verbose:
201
+ print(f"Fixed answer: {fixed_answer}")
89
202
  return proposed_data
90
203
  except Exception as e:
91
204
  if verbose:
92
- print(f"Proposed solution {proposed_data} is invalid. Error: {e}")
93
- return response
205
+ print(f"Validation failed for fixed answer: {e}")
206
+
207
+ # Strategy 2: Check if the answer is a match when normalized (strip whitespace)
208
+ response_text_normalized = response_text.strip()
209
+ for option in self.question_options:
210
+ option_str = str(option).strip()
211
+ if option_str == response_text_normalized:
212
+ if verbose:
213
+ print(f"Normalized match found with option: {option}")
214
+ proposed_data = {
215
+ "answer": option, # Use the exact option from the list
216
+ "comment": response.get("comment"),
217
+ "generated_tokens": response.get("generated_tokens"),
218
+ }
219
+ try:
220
+ self.response_model.model_validate(proposed_data)
221
+ if verbose:
222
+ print(f"Fixed answer with normalization: {option}")
223
+ return proposed_data
224
+ except Exception as e:
225
+ if verbose:
226
+ print(f"Validation failed for normalized answer: {e}")
227
+
228
+ # Strategy 3: Check if the answer is a prefix of any option
229
+ # This handles cases where the model returns a partial answer
230
+ # Only apply this strategy if we have a meaningful response text
231
+ if response_text_normalized and not response_text_normalized.lower() == "none":
232
+ for option in self.question_options:
233
+ option_str = str(option).strip()
234
+ if option_str.startswith(response_text_normalized) or response_text_normalized.startswith(option_str):
235
+ if verbose:
236
+ print(f"Prefix match found with option: {option}")
237
+ proposed_data = {
238
+ "answer": option, # Use the exact option from the list
239
+ "comment": response.get("comment"),
240
+ "generated_tokens": response.get("generated_tokens"),
241
+ }
242
+ try:
243
+ self.response_model.model_validate(proposed_data)
244
+ if verbose:
245
+ print(f"Fixed answer with prefix matching: {option}")
246
+ return proposed_data
247
+ except Exception as e:
248
+ if verbose:
249
+ print(f"Validation failed for prefix answer: {e}")
250
+
251
+ # If multiple or no matches, return original response
252
+ if verbose:
253
+ if len(matches) > 1:
254
+ print(f"Multiple matches found: {matches}, cannot determine correct option")
255
+ else:
256
+ print("No matches found in response text")
257
+
258
+ return response
94
259
 
95
260
  valid_examples = [
96
- ({"answer": 1}, {"question_options": ["Good", "Great", "OK", "Bad"]})
261
+ ({"answer": "Good"}, {"question_options": ["Good", "Great", "OK", "Bad"]})
97
262
  ]
98
263
 
99
264
  invalid_examples = [
100
265
  (
101
- {"answer": -1},
266
+ {"answer": "Terrible"},
102
267
  {"question_options": ["Good", "Great", "OK", "Bad"]},
103
- "Answer code must be a non-negative integer",
268
+ "Value error, Permitted values are 'Good', 'Great', 'OK', 'Bad'",
104
269
  ),
105
270
  (
106
271
  {"answer": None},
107
272
  {"question_options": ["Good", "Great", "OK", "Bad"]},
108
- "Answer code must not be missing.",
273
+ "Answer must not be null",
109
274
  ),
110
275
  ]
111
276
 
@@ -314,7 +479,8 @@ class QuestionMultipleChoice(QuestionBase):
314
479
 
315
480
  if potential_replacement is None:
316
481
  # Nope - maybe it's in the substition dict?
317
- raise ValueError(
482
+ from .exceptions import QuestionValueError
483
+ raise QuestionValueError(
318
484
  f"Could not find the key '{question_option_key}' in the scenario."
319
485
  f"The substition dict was: '{substitution_dict}.'"
320
486
  f"The question options were: '{question_options}'."
@@ -353,11 +519,13 @@ class QuestionMultipleChoice(QuestionBase):
353
519
  try:
354
520
  return translated_options[int(answer_code)]
355
521
  except IndexError:
356
- raise ValueError(
522
+ from .exceptions import QuestionValueError
523
+ raise QuestionValueError(
357
524
  f"Answer code is out of range. The answer code index was: {int(answer_code)}. The options were: {translated_options}."
358
525
  )
359
526
  except TypeError:
360
- raise ValueError(
527
+ from .exceptions import QuestionValueError
528
+ raise QuestionValueError(
361
529
  f"The answer code was: '{answer_code}.'",
362
530
  f"The options were: '{translated_options}'.",
363
531
  )