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.
- edsl/__init__.py +124 -53
- edsl/__version__.py +1 -1
- edsl/agents/agent.py +21 -21
- edsl/agents/agent_list.py +2 -5
- edsl/agents/exceptions.py +119 -5
- edsl/base/__init__.py +10 -35
- edsl/base/base_class.py +71 -36
- edsl/base/base_exception.py +204 -0
- edsl/base/data_transfer_models.py +1 -1
- edsl/base/exceptions.py +94 -0
- edsl/buckets/__init__.py +15 -1
- edsl/buckets/bucket_collection.py +3 -4
- edsl/buckets/exceptions.py +107 -0
- edsl/buckets/model_buckets.py +1 -2
- edsl/buckets/token_bucket.py +11 -6
- edsl/buckets/token_bucket_api.py +27 -12
- edsl/buckets/token_bucket_client.py +9 -7
- edsl/caching/cache.py +12 -4
- edsl/caching/cache_entry.py +10 -9
- edsl/caching/exceptions.py +113 -7
- edsl/caching/remote_cache_sync.py +6 -7
- edsl/caching/sql_dict.py +20 -14
- edsl/cli.py +43 -0
- edsl/config/__init__.py +1 -1
- edsl/config/config_class.py +32 -6
- edsl/conversation/Conversation.py +8 -4
- edsl/conversation/car_buying.py +1 -3
- edsl/conversation/exceptions.py +58 -0
- edsl/conversation/mug_negotiation.py +2 -8
- edsl/coop/__init__.py +28 -6
- edsl/coop/coop.py +120 -29
- edsl/coop/coop_functions.py +1 -1
- edsl/coop/ep_key_handling.py +1 -1
- edsl/coop/exceptions.py +188 -9
- edsl/coop/price_fetcher.py +5 -8
- edsl/coop/utils.py +4 -6
- edsl/dataset/__init__.py +5 -4
- edsl/dataset/dataset.py +177 -86
- edsl/dataset/dataset_operations_mixin.py +98 -76
- edsl/dataset/dataset_tree.py +11 -7
- edsl/dataset/display/table_display.py +0 -2
- edsl/dataset/display/table_renderers.py +6 -4
- edsl/dataset/exceptions.py +125 -0
- edsl/dataset/file_exports.py +18 -11
- edsl/dataset/r/ggplot.py +13 -6
- edsl/display/__init__.py +27 -0
- edsl/display/core.py +147 -0
- edsl/display/plugin.py +189 -0
- edsl/display/utils.py +52 -0
- edsl/inference_services/__init__.py +9 -1
- edsl/inference_services/available_model_cache_handler.py +1 -1
- edsl/inference_services/available_model_fetcher.py +5 -6
- edsl/inference_services/data_structures.py +10 -7
- edsl/inference_services/exceptions.py +132 -1
- edsl/inference_services/inference_service_abc.py +2 -2
- edsl/inference_services/inference_services_collection.py +2 -6
- edsl/inference_services/registry.py +4 -3
- edsl/inference_services/service_availability.py +4 -3
- edsl/inference_services/services/anthropic_service.py +4 -1
- edsl/inference_services/services/aws_bedrock.py +13 -12
- edsl/inference_services/services/azure_ai.py +12 -10
- edsl/inference_services/services/deep_infra_service.py +1 -4
- edsl/inference_services/services/deep_seek_service.py +1 -5
- edsl/inference_services/services/google_service.py +7 -3
- edsl/inference_services/services/groq_service.py +1 -1
- edsl/inference_services/services/mistral_ai_service.py +4 -2
- edsl/inference_services/services/ollama_service.py +1 -1
- edsl/inference_services/services/open_ai_service.py +7 -5
- edsl/inference_services/services/perplexity_service.py +6 -2
- edsl/inference_services/services/test_service.py +8 -7
- edsl/inference_services/services/together_ai_service.py +2 -3
- edsl/inference_services/services/xai_service.py +1 -1
- edsl/instructions/__init__.py +1 -1
- edsl/instructions/change_instruction.py +7 -5
- edsl/instructions/exceptions.py +61 -0
- edsl/instructions/instruction.py +6 -2
- edsl/instructions/instruction_collection.py +6 -4
- edsl/instructions/instruction_handler.py +12 -15
- edsl/interviews/ReportErrors.py +0 -3
- edsl/interviews/__init__.py +9 -2
- edsl/interviews/answering_function.py +11 -13
- edsl/interviews/exception_tracking.py +15 -8
- edsl/interviews/exceptions.py +79 -0
- edsl/interviews/interview.py +33 -30
- edsl/interviews/interview_status_dictionary.py +4 -2
- edsl/interviews/interview_status_log.py +2 -1
- edsl/interviews/interview_task_manager.py +5 -5
- edsl/interviews/request_token_estimator.py +5 -2
- edsl/interviews/statistics.py +3 -4
- edsl/invigilators/__init__.py +7 -1
- edsl/invigilators/exceptions.py +79 -0
- edsl/invigilators/invigilator_base.py +0 -1
- edsl/invigilators/invigilators.py +9 -13
- edsl/invigilators/prompt_constructor.py +1 -5
- edsl/invigilators/prompt_helpers.py +8 -4
- edsl/invigilators/question_instructions_prompt_builder.py +1 -1
- edsl/invigilators/question_option_processor.py +9 -5
- edsl/invigilators/question_template_replacements_builder.py +3 -2
- edsl/jobs/__init__.py +42 -5
- edsl/jobs/async_interview_runner.py +25 -23
- edsl/jobs/check_survey_scenario_compatibility.py +11 -10
- edsl/jobs/data_structures.py +8 -5
- edsl/jobs/exceptions.py +177 -8
- edsl/jobs/fetch_invigilator.py +1 -1
- edsl/jobs/jobs.py +74 -69
- edsl/jobs/jobs_checks.py +6 -7
- edsl/jobs/jobs_component_constructor.py +4 -4
- edsl/jobs/jobs_pricing_estimation.py +4 -3
- edsl/jobs/jobs_remote_inference_logger.py +5 -4
- edsl/jobs/jobs_runner_asyncio.py +3 -4
- edsl/jobs/jobs_runner_status.py +8 -9
- edsl/jobs/remote_inference.py +27 -24
- edsl/jobs/results_exceptions_handler.py +10 -7
- edsl/key_management/__init__.py +3 -1
- edsl/key_management/exceptions.py +62 -0
- edsl/key_management/key_lookup.py +1 -1
- edsl/key_management/key_lookup_builder.py +37 -14
- edsl/key_management/key_lookup_collection.py +2 -0
- edsl/language_models/__init__.py +1 -1
- edsl/language_models/exceptions.py +302 -14
- edsl/language_models/language_model.py +9 -8
- edsl/language_models/model.py +4 -4
- edsl/language_models/model_list.py +1 -1
- edsl/language_models/price_manager.py +1 -1
- edsl/language_models/raw_response_handler.py +14 -9
- edsl/language_models/registry.py +17 -21
- edsl/language_models/repair.py +0 -6
- edsl/language_models/unused/fake_openai_service.py +0 -1
- edsl/load_plugins.py +69 -0
- edsl/logger.py +146 -0
- edsl/notebooks/__init__.py +24 -1
- edsl/notebooks/exceptions.py +82 -0
- edsl/notebooks/notebook.py +7 -3
- edsl/notebooks/notebook_to_latex.py +1 -2
- edsl/plugins/__init__.py +63 -0
- edsl/plugins/built_in/export_example.py +50 -0
- edsl/plugins/built_in/pig_latin.py +67 -0
- edsl/plugins/cli.py +372 -0
- edsl/plugins/cli_typer.py +283 -0
- edsl/plugins/exceptions.py +31 -0
- edsl/plugins/hookspec.py +51 -0
- edsl/plugins/plugin_host.py +128 -0
- edsl/plugins/plugin_manager.py +633 -0
- edsl/plugins/plugins_registry.py +168 -0
- edsl/prompts/__init__.py +24 -1
- edsl/prompts/exceptions.py +107 -5
- edsl/prompts/prompt.py +15 -7
- edsl/questions/HTMLQuestion.py +5 -11
- edsl/questions/Quick.py +0 -1
- edsl/questions/__init__.py +6 -4
- edsl/questions/answer_validator_mixin.py +318 -323
- edsl/questions/compose_questions.py +3 -3
- edsl/questions/descriptors.py +11 -50
- edsl/questions/exceptions.py +278 -22
- edsl/questions/loop_processor.py +7 -5
- edsl/questions/prompt_templates/question_list.jinja +3 -0
- edsl/questions/question_base.py +46 -19
- edsl/questions/question_base_gen_mixin.py +2 -2
- edsl/questions/question_base_prompts_mixin.py +13 -7
- edsl/questions/question_budget.py +503 -98
- edsl/questions/question_check_box.py +660 -160
- edsl/questions/question_dict.py +345 -194
- edsl/questions/question_extract.py +401 -61
- edsl/questions/question_free_text.py +80 -14
- edsl/questions/question_functional.py +119 -9
- edsl/questions/{derived/question_likert_five.py → question_likert_five.py} +2 -2
- edsl/questions/{derived/question_linear_scale.py → question_linear_scale.py} +3 -4
- edsl/questions/question_list.py +275 -28
- edsl/questions/question_matrix.py +643 -96
- edsl/questions/question_multiple_choice.py +219 -51
- edsl/questions/question_numerical.py +361 -32
- edsl/questions/question_rank.py +401 -124
- edsl/questions/question_registry.py +7 -5
- edsl/questions/{derived/question_top_k.py → question_top_k.py} +3 -3
- edsl/questions/{derived/question_yes_no.py → question_yes_no.py} +3 -4
- edsl/questions/register_questions_meta.py +2 -2
- edsl/questions/response_validator_abc.py +13 -15
- edsl/questions/response_validator_factory.py +10 -12
- edsl/questions/templates/dict/answering_instructions.jinja +1 -0
- edsl/questions/templates/rank/question_presentation.jinja +1 -1
- edsl/results/__init__.py +1 -1
- edsl/results/exceptions.py +141 -7
- edsl/results/report.py +1 -2
- edsl/results/result.py +11 -9
- edsl/results/results.py +480 -321
- edsl/results/results_selector.py +8 -4
- edsl/scenarios/PdfExtractor.py +2 -2
- edsl/scenarios/construct_download_link.py +69 -35
- edsl/scenarios/directory_scanner.py +33 -14
- edsl/scenarios/document_chunker.py +1 -1
- edsl/scenarios/exceptions.py +238 -14
- edsl/scenarios/file_methods.py +1 -1
- edsl/scenarios/file_store.py +7 -3
- edsl/scenarios/handlers/__init__.py +17 -0
- edsl/scenarios/handlers/docx_file_store.py +0 -5
- edsl/scenarios/handlers/pdf_file_store.py +0 -1
- edsl/scenarios/handlers/pptx_file_store.py +0 -5
- edsl/scenarios/handlers/py_file_store.py +0 -1
- edsl/scenarios/handlers/sql_file_store.py +1 -4
- edsl/scenarios/handlers/sqlite_file_store.py +0 -1
- edsl/scenarios/handlers/txt_file_store.py +1 -1
- edsl/scenarios/scenario.py +1 -3
- edsl/scenarios/scenario_list.py +179 -27
- edsl/scenarios/scenario_list_pdf_tools.py +1 -0
- edsl/scenarios/scenario_selector.py +0 -1
- edsl/surveys/__init__.py +3 -4
- edsl/surveys/dag/__init__.py +4 -2
- edsl/surveys/descriptors.py +1 -1
- edsl/surveys/edit_survey.py +1 -0
- edsl/surveys/exceptions.py +165 -9
- edsl/surveys/memory/__init__.py +5 -3
- edsl/surveys/memory/memory_management.py +1 -0
- edsl/surveys/memory/memory_plan.py +6 -15
- edsl/surveys/rules/__init__.py +5 -3
- edsl/surveys/rules/rule.py +1 -2
- edsl/surveys/rules/rule_collection.py +1 -1
- edsl/surveys/survey.py +12 -24
- edsl/surveys/survey_css.py +3 -3
- edsl/surveys/survey_export.py +6 -3
- edsl/surveys/survey_flow_visualization.py +10 -1
- edsl/surveys/survey_simulator.py +2 -1
- edsl/tasks/__init__.py +23 -1
- edsl/tasks/exceptions.py +72 -0
- edsl/tasks/question_task_creator.py +3 -3
- edsl/tasks/task_creators.py +1 -3
- edsl/tasks/task_history.py +8 -10
- edsl/tasks/task_status_log.py +1 -2
- edsl/tokens/__init__.py +29 -1
- edsl/tokens/exceptions.py +37 -0
- edsl/tokens/interview_token_usage.py +3 -2
- edsl/tokens/token_usage.py +4 -3
- edsl/utilities/__init__.py +21 -1
- edsl/utilities/decorators.py +1 -2
- edsl/utilities/markdown_to_docx.py +2 -2
- edsl/utilities/markdown_to_pdf.py +1 -1
- edsl/utilities/repair_functions.py +0 -1
- edsl/utilities/restricted_python.py +0 -1
- edsl/utilities/template_loader.py +2 -3
- edsl/utilities/utilities.py +8 -29
- {edsl-0.1.49.dist-info → edsl-0.1.51.dist-info}/METADATA +32 -2
- edsl-0.1.51.dist-info/RECORD +365 -0
- edsl-0.1.51.dist-info/entry_points.txt +3 -0
- edsl/dataset/smart_objects.py +0 -96
- edsl/exceptions/BaseException.py +0 -21
- edsl/exceptions/__init__.py +0 -54
- edsl/exceptions/configuration.py +0 -16
- edsl/exceptions/general.py +0 -34
- edsl/questions/derived/__init__.py +0 -0
- edsl/study/ObjectEntry.py +0 -173
- edsl/study/ProofOfWork.py +0 -113
- edsl/study/SnapShot.py +0 -80
- edsl/study/Study.py +0 -520
- edsl/study/__init__.py +0 -6
- edsl/utilities/interface.py +0 -135
- edsl-0.1.49.dist-info/RECORD +0 -347
- {edsl-0.1.49.dist-info → edsl-0.1.51.dist-info}/LICENSE +0 -0
- {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
|
-
|
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
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
64
|
-
|
65
|
-
|
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
|
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
|
72
|
-
|
73
|
-
|
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 "
|
184
|
+
print(f"Match found with option: {option_str}")
|
77
185
|
if option not in matches:
|
78
186
|
matches.append(option)
|
79
187
|
|
80
|
-
|
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":
|
85
|
-
"
|
192
|
+
"answer": fixed_answer,
|
193
|
+
"comment": response.get("comment"),
|
194
|
+
"generated_tokens": response.get("generated_tokens"),
|
86
195
|
}
|
196
|
+
|
87
197
|
try:
|
88
|
-
|
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"
|
93
|
-
|
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":
|
261
|
+
({"answer": "Good"}, {"question_options": ["Good", "Great", "OK", "Bad"]})
|
97
262
|
]
|
98
263
|
|
99
264
|
invalid_examples = [
|
100
265
|
(
|
101
|
-
{"answer":
|
266
|
+
{"answer": "Terrible"},
|
102
267
|
{"question_options": ["Good", "Great", "OK", "Bad"]},
|
103
|
-
"
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
)
|