edsl 0.1.38.dev4__py3-none-any.whl → 0.1.39__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/Base.py +197 -116
- edsl/__init__.py +15 -7
- edsl/__version__.py +1 -1
- edsl/agents/Agent.py +351 -147
- edsl/agents/AgentList.py +211 -73
- edsl/agents/Invigilator.py +101 -50
- edsl/agents/InvigilatorBase.py +62 -70
- edsl/agents/PromptConstructor.py +143 -225
- edsl/agents/QuestionInstructionPromptBuilder.py +128 -0
- edsl/agents/QuestionTemplateReplacementsBuilder.py +137 -0
- edsl/agents/__init__.py +0 -1
- edsl/agents/prompt_helpers.py +3 -3
- edsl/agents/question_option_processor.py +172 -0
- edsl/auto/AutoStudy.py +18 -5
- edsl/auto/StageBase.py +53 -40
- edsl/auto/StageQuestions.py +2 -1
- edsl/auto/utilities.py +0 -6
- edsl/config.py +22 -2
- edsl/conversation/car_buying.py +2 -1
- edsl/coop/CoopFunctionsMixin.py +15 -0
- edsl/coop/ExpectedParrotKeyHandler.py +125 -0
- edsl/coop/PriceFetcher.py +1 -1
- edsl/coop/coop.py +125 -47
- edsl/coop/utils.py +14 -14
- edsl/data/Cache.py +45 -27
- edsl/data/CacheEntry.py +12 -15
- edsl/data/CacheHandler.py +31 -12
- edsl/data/RemoteCacheSync.py +154 -46
- edsl/data/__init__.py +4 -3
- edsl/data_transfer_models.py +2 -1
- edsl/enums.py +27 -0
- edsl/exceptions/__init__.py +50 -50
- edsl/exceptions/agents.py +12 -0
- edsl/exceptions/inference_services.py +5 -0
- edsl/exceptions/questions.py +24 -6
- edsl/exceptions/scenarios.py +7 -0
- edsl/inference_services/AnthropicService.py +38 -19
- edsl/inference_services/AvailableModelCacheHandler.py +184 -0
- edsl/inference_services/AvailableModelFetcher.py +215 -0
- edsl/inference_services/AwsBedrock.py +0 -2
- edsl/inference_services/AzureAI.py +0 -2
- edsl/inference_services/GoogleService.py +7 -12
- edsl/inference_services/InferenceServiceABC.py +18 -85
- edsl/inference_services/InferenceServicesCollection.py +120 -79
- edsl/inference_services/MistralAIService.py +0 -3
- edsl/inference_services/OpenAIService.py +47 -35
- edsl/inference_services/PerplexityService.py +0 -3
- edsl/inference_services/ServiceAvailability.py +135 -0
- edsl/inference_services/TestService.py +11 -10
- edsl/inference_services/TogetherAIService.py +5 -3
- edsl/inference_services/data_structures.py +134 -0
- edsl/jobs/AnswerQuestionFunctionConstructor.py +223 -0
- edsl/jobs/Answers.py +1 -14
- edsl/jobs/FetchInvigilator.py +47 -0
- edsl/jobs/InterviewTaskManager.py +98 -0
- edsl/jobs/InterviewsConstructor.py +50 -0
- edsl/jobs/Jobs.py +356 -431
- edsl/jobs/JobsChecks.py +35 -10
- edsl/jobs/JobsComponentConstructor.py +189 -0
- edsl/jobs/JobsPrompts.py +6 -4
- edsl/jobs/JobsRemoteInferenceHandler.py +205 -133
- edsl/jobs/JobsRemoteInferenceLogger.py +239 -0
- edsl/jobs/RequestTokenEstimator.py +30 -0
- edsl/jobs/async_interview_runner.py +138 -0
- edsl/jobs/buckets/BucketCollection.py +44 -3
- edsl/jobs/buckets/TokenBucket.py +53 -21
- edsl/jobs/buckets/TokenBucketAPI.py +211 -0
- edsl/jobs/buckets/TokenBucketClient.py +191 -0
- edsl/jobs/check_survey_scenario_compatibility.py +85 -0
- edsl/jobs/data_structures.py +120 -0
- edsl/jobs/decorators.py +35 -0
- edsl/jobs/interviews/Interview.py +143 -408
- edsl/jobs/jobs_status_enums.py +9 -0
- edsl/jobs/loggers/HTMLTableJobLogger.py +304 -0
- edsl/jobs/results_exceptions_handler.py +98 -0
- edsl/jobs/runners/JobsRunnerAsyncio.py +88 -403
- edsl/jobs/runners/JobsRunnerStatus.py +133 -165
- edsl/jobs/tasks/QuestionTaskCreator.py +21 -19
- edsl/jobs/tasks/TaskHistory.py +38 -18
- edsl/jobs/tasks/task_status_enum.py +0 -2
- edsl/language_models/ComputeCost.py +63 -0
- edsl/language_models/LanguageModel.py +194 -236
- edsl/language_models/ModelList.py +28 -19
- edsl/language_models/PriceManager.py +127 -0
- edsl/language_models/RawResponseHandler.py +106 -0
- edsl/language_models/ServiceDataSources.py +0 -0
- edsl/language_models/__init__.py +1 -2
- edsl/language_models/key_management/KeyLookup.py +63 -0
- edsl/language_models/key_management/KeyLookupBuilder.py +273 -0
- edsl/language_models/key_management/KeyLookupCollection.py +38 -0
- edsl/language_models/key_management/__init__.py +0 -0
- edsl/language_models/key_management/models.py +131 -0
- edsl/language_models/model.py +256 -0
- edsl/language_models/repair.py +2 -2
- edsl/language_models/utilities.py +5 -4
- edsl/notebooks/Notebook.py +19 -14
- edsl/notebooks/NotebookToLaTeX.py +142 -0
- edsl/prompts/Prompt.py +29 -39
- edsl/questions/ExceptionExplainer.py +77 -0
- edsl/questions/HTMLQuestion.py +103 -0
- edsl/questions/QuestionBase.py +68 -214
- edsl/questions/QuestionBasePromptsMixin.py +7 -3
- edsl/questions/QuestionBudget.py +1 -1
- edsl/questions/QuestionCheckBox.py +3 -3
- edsl/questions/QuestionExtract.py +5 -7
- edsl/questions/QuestionFreeText.py +2 -3
- edsl/questions/QuestionList.py +10 -18
- edsl/questions/QuestionMatrix.py +265 -0
- edsl/questions/QuestionMultipleChoice.py +67 -23
- edsl/questions/QuestionNumerical.py +2 -4
- edsl/questions/QuestionRank.py +7 -17
- edsl/questions/SimpleAskMixin.py +4 -3
- edsl/questions/__init__.py +2 -1
- edsl/questions/{AnswerValidatorMixin.py → answer_validator_mixin.py} +47 -2
- edsl/questions/data_structures.py +20 -0
- edsl/questions/derived/QuestionLinearScale.py +6 -3
- edsl/questions/derived/QuestionTopK.py +1 -1
- edsl/questions/descriptors.py +17 -3
- edsl/questions/loop_processor.py +149 -0
- edsl/questions/{QuestionBaseGenMixin.py → question_base_gen_mixin.py} +57 -50
- edsl/questions/question_registry.py +1 -1
- edsl/questions/{ResponseValidatorABC.py → response_validator_abc.py} +40 -26
- edsl/questions/response_validator_factory.py +34 -0
- edsl/questions/templates/matrix/__init__.py +1 -0
- edsl/questions/templates/matrix/answering_instructions.jinja +5 -0
- edsl/questions/templates/matrix/question_presentation.jinja +20 -0
- edsl/results/CSSParameterizer.py +1 -1
- edsl/results/Dataset.py +170 -7
- edsl/results/DatasetExportMixin.py +168 -305
- edsl/results/DatasetTree.py +28 -8
- edsl/results/MarkdownToDocx.py +122 -0
- edsl/results/MarkdownToPDF.py +111 -0
- edsl/results/Result.py +298 -206
- edsl/results/Results.py +149 -131
- edsl/results/ResultsExportMixin.py +2 -0
- edsl/results/TableDisplay.py +98 -171
- edsl/results/TextEditor.py +50 -0
- edsl/results/__init__.py +1 -1
- edsl/results/file_exports.py +252 -0
- edsl/results/{Selector.py → results_selector.py} +23 -13
- edsl/results/smart_objects.py +96 -0
- edsl/results/table_data_class.py +12 -0
- edsl/results/table_renderers.py +118 -0
- edsl/scenarios/ConstructDownloadLink.py +109 -0
- edsl/scenarios/DocumentChunker.py +102 -0
- edsl/scenarios/DocxScenario.py +16 -0
- edsl/scenarios/FileStore.py +150 -239
- edsl/scenarios/PdfExtractor.py +40 -0
- edsl/scenarios/Scenario.py +90 -193
- edsl/scenarios/ScenarioHtmlMixin.py +4 -3
- edsl/scenarios/ScenarioList.py +415 -244
- edsl/scenarios/ScenarioListExportMixin.py +0 -7
- edsl/scenarios/ScenarioListPdfMixin.py +15 -37
- edsl/scenarios/__init__.py +1 -2
- edsl/scenarios/directory_scanner.py +96 -0
- edsl/scenarios/file_methods.py +85 -0
- edsl/scenarios/handlers/__init__.py +13 -0
- edsl/scenarios/handlers/csv.py +49 -0
- edsl/scenarios/handlers/docx.py +76 -0
- edsl/scenarios/handlers/html.py +37 -0
- edsl/scenarios/handlers/json.py +111 -0
- edsl/scenarios/handlers/latex.py +5 -0
- edsl/scenarios/handlers/md.py +51 -0
- edsl/scenarios/handlers/pdf.py +68 -0
- edsl/scenarios/handlers/png.py +39 -0
- edsl/scenarios/handlers/pptx.py +105 -0
- edsl/scenarios/handlers/py.py +294 -0
- edsl/scenarios/handlers/sql.py +313 -0
- edsl/scenarios/handlers/sqlite.py +149 -0
- edsl/scenarios/handlers/txt.py +33 -0
- edsl/scenarios/{ScenarioJoin.py → scenario_join.py} +10 -6
- edsl/scenarios/scenario_selector.py +156 -0
- edsl/study/ObjectEntry.py +1 -1
- edsl/study/SnapShot.py +1 -1
- edsl/study/Study.py +5 -12
- edsl/surveys/ConstructDAG.py +92 -0
- edsl/surveys/EditSurvey.py +221 -0
- edsl/surveys/InstructionHandler.py +100 -0
- edsl/surveys/MemoryManagement.py +72 -0
- edsl/surveys/Rule.py +5 -4
- edsl/surveys/RuleCollection.py +25 -27
- edsl/surveys/RuleManager.py +172 -0
- edsl/surveys/Simulator.py +75 -0
- edsl/surveys/Survey.py +270 -791
- edsl/surveys/SurveyCSS.py +20 -8
- edsl/surveys/{SurveyFlowVisualizationMixin.py → SurveyFlowVisualization.py} +11 -9
- edsl/surveys/SurveyToApp.py +141 -0
- edsl/surveys/__init__.py +4 -2
- edsl/surveys/descriptors.py +6 -2
- edsl/surveys/instructions/ChangeInstruction.py +1 -2
- edsl/surveys/instructions/Instruction.py +4 -13
- edsl/surveys/instructions/InstructionCollection.py +11 -6
- edsl/templates/error_reporting/interview_details.html +1 -1
- edsl/templates/error_reporting/report.html +1 -1
- edsl/tools/plotting.py +1 -1
- edsl/utilities/PrettyList.py +56 -0
- edsl/utilities/is_notebook.py +18 -0
- edsl/utilities/is_valid_variable_name.py +11 -0
- edsl/utilities/remove_edsl_version.py +24 -0
- edsl/utilities/utilities.py +35 -23
- {edsl-0.1.38.dev4.dist-info → edsl-0.1.39.dist-info}/METADATA +12 -10
- edsl-0.1.39.dist-info/RECORD +358 -0
- {edsl-0.1.38.dev4.dist-info → edsl-0.1.39.dist-info}/WHEEL +1 -1
- edsl/language_models/KeyLookup.py +0 -30
- edsl/language_models/registry.py +0 -190
- edsl/language_models/unused/ReplicateBase.py +0 -83
- edsl/results/ResultsDBMixin.py +0 -238
- edsl-0.1.38.dev4.dist-info/RECORD +0 -277
- /edsl/questions/{RegisterQuestionsMeta.py → register_questions_meta.py} +0 -0
- /edsl/results/{ResultsFetchMixin.py → results_fetch_mixin.py} +0 -0
- /edsl/results/{ResultsToolsMixin.py → results_tools_mixin.py} +0 -0
- {edsl-0.1.38.dev4.dist-info → edsl-0.1.39.dist-info}/LICENSE +0 -0
@@ -0,0 +1,149 @@
|
|
1
|
+
from typing import List, Any, Dict, Union
|
2
|
+
from jinja2 import Environment
|
3
|
+
from edsl.questions.QuestionBase import QuestionBase
|
4
|
+
from edsl import ScenarioList
|
5
|
+
|
6
|
+
|
7
|
+
class LoopProcessor:
|
8
|
+
def __init__(self, question: QuestionBase):
|
9
|
+
self.question = question
|
10
|
+
self.env = Environment()
|
11
|
+
|
12
|
+
def process_templates(self, scenario_list: ScenarioList) -> List[QuestionBase]:
|
13
|
+
"""Process templates for each scenario and return list of modified questions.
|
14
|
+
|
15
|
+
Args:
|
16
|
+
scenario_list: List of scenarios to process templates against
|
17
|
+
|
18
|
+
Returns:
|
19
|
+
List of QuestionBase objects with rendered templates
|
20
|
+
"""
|
21
|
+
questions = []
|
22
|
+
starting_name = self.question.question_name
|
23
|
+
|
24
|
+
for index, scenario in enumerate(scenario_list):
|
25
|
+
question_data = self.question.to_dict().copy()
|
26
|
+
processed_data = self._process_data(question_data, scenario)
|
27
|
+
|
28
|
+
if processed_data["question_name"] == starting_name:
|
29
|
+
processed_data["question_name"] += f"_{index}"
|
30
|
+
|
31
|
+
questions.append(QuestionBase.from_dict(processed_data))
|
32
|
+
|
33
|
+
return questions
|
34
|
+
|
35
|
+
def _process_data(
|
36
|
+
self, data: Dict[str, Any], scenario: Dict[str, Any]
|
37
|
+
) -> Dict[str, Any]:
|
38
|
+
"""Process all data fields according to their type.
|
39
|
+
|
40
|
+
Args:
|
41
|
+
data: Dictionary of question data
|
42
|
+
scenario: Current scenario to render templates against
|
43
|
+
|
44
|
+
Returns:
|
45
|
+
Processed dictionary with rendered templates
|
46
|
+
"""
|
47
|
+
processed = {}
|
48
|
+
|
49
|
+
for key, value in [(k, v) for k, v in data.items() if v is not None]:
|
50
|
+
processed[key] = self._process_value(key, value, scenario)
|
51
|
+
|
52
|
+
return processed
|
53
|
+
|
54
|
+
def _process_value(self, key: str, value: Any, scenario: Dict[str, Any]) -> Any:
|
55
|
+
"""Process a single value according to its type.
|
56
|
+
|
57
|
+
Args:
|
58
|
+
key: Dictionary key
|
59
|
+
value: Value to process
|
60
|
+
scenario: Current scenario
|
61
|
+
|
62
|
+
Returns:
|
63
|
+
Processed value
|
64
|
+
"""
|
65
|
+
if key == "question_options" and isinstance(value, str):
|
66
|
+
return value
|
67
|
+
|
68
|
+
if key == "option_labels":
|
69
|
+
import json
|
70
|
+
|
71
|
+
return (
|
72
|
+
eval(self._render_template(value, scenario))
|
73
|
+
if isinstance(value, str)
|
74
|
+
else value
|
75
|
+
)
|
76
|
+
|
77
|
+
if isinstance(value, str):
|
78
|
+
return self._render_template(value, scenario)
|
79
|
+
|
80
|
+
if isinstance(value, list):
|
81
|
+
return self._process_list(value, scenario)
|
82
|
+
|
83
|
+
if isinstance(value, dict):
|
84
|
+
return self._process_dict(value, scenario)
|
85
|
+
|
86
|
+
if isinstance(value, (int, float)):
|
87
|
+
return value
|
88
|
+
|
89
|
+
raise ValueError(f"Unexpected value type: {type(value)} for key '{key}'")
|
90
|
+
|
91
|
+
def _render_template(self, template: str, scenario: Dict[str, Any]) -> str:
|
92
|
+
"""Render a single template string.
|
93
|
+
|
94
|
+
Args:
|
95
|
+
template: Template string to render
|
96
|
+
scenario: Current scenario
|
97
|
+
|
98
|
+
Returns:
|
99
|
+
Rendered template string
|
100
|
+
"""
|
101
|
+
return self.env.from_string(template).render(scenario)
|
102
|
+
|
103
|
+
def _process_list(self, items: List[Any], scenario: Dict[str, Any]) -> List[Any]:
|
104
|
+
"""Process all items in a list.
|
105
|
+
|
106
|
+
Args:
|
107
|
+
items: List of items to process
|
108
|
+
scenario: Current scenario
|
109
|
+
|
110
|
+
Returns:
|
111
|
+
List of processed items
|
112
|
+
"""
|
113
|
+
return [
|
114
|
+
self._render_template(item, scenario) if isinstance(item, str) else item
|
115
|
+
for item in items
|
116
|
+
]
|
117
|
+
|
118
|
+
def _process_dict(
|
119
|
+
self, data: Dict[str, Any], scenario: Dict[str, Any]
|
120
|
+
) -> Dict[str, Any]:
|
121
|
+
"""Process all keys and values in a dictionary.
|
122
|
+
|
123
|
+
Args:
|
124
|
+
data: Dictionary to process
|
125
|
+
scenario: Current scenario
|
126
|
+
|
127
|
+
Returns:
|
128
|
+
Dictionary with processed keys and values
|
129
|
+
"""
|
130
|
+
return {
|
131
|
+
(self._render_template(k, scenario) if isinstance(k, str) else k): (
|
132
|
+
self._render_template(v, scenario) if isinstance(v, str) else v
|
133
|
+
)
|
134
|
+
for k, v in data.items()
|
135
|
+
}
|
136
|
+
|
137
|
+
|
138
|
+
# Usage example:
|
139
|
+
"""
|
140
|
+
from edsl import QuestionFreeText, ScenarioList
|
141
|
+
|
142
|
+
question = QuestionFreeText(
|
143
|
+
question_text="What are your thoughts on: {{subject}}?",
|
144
|
+
question_name="base_{{subject}}"
|
145
|
+
)
|
146
|
+
processor = TemplateProcessor(question)
|
147
|
+
scenarios = ScenarioList.from_list("subject", ["Math", "Economics", "Chemistry"])
|
148
|
+
processed_questions = processor.process_templates(scenarios)
|
149
|
+
"""
|
@@ -1,11 +1,16 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
import copy
|
3
3
|
import itertools
|
4
|
-
from typing import Optional, List, Callable, Type
|
5
|
-
|
4
|
+
from typing import Optional, List, Callable, Type, TYPE_CHECKING
|
5
|
+
|
6
|
+
if TYPE_CHECKING:
|
7
|
+
from edsl.questions.QuestionBase import QuestionBase
|
8
|
+
from edsl.scenarios.ScenarioList import ScenarioList
|
6
9
|
|
7
10
|
|
8
11
|
class QuestionBaseGenMixin:
|
12
|
+
"""Mixin for QuestionBase."""
|
13
|
+
|
9
14
|
def copy(self) -> QuestionBase:
|
10
15
|
"""Return a deep copy of the question.
|
11
16
|
|
@@ -21,7 +26,7 @@ class QuestionBaseGenMixin:
|
|
21
26
|
def option_permutations(self) -> list[QuestionBase]:
|
22
27
|
"""Return a list of questions with all possible permutations of the options.
|
23
28
|
|
24
|
-
>>> from edsl import QuestionMultipleChoice as Q
|
29
|
+
>>> from edsl.questions.QuestionMultipleChoice import QuestionMultipleChoice as Q
|
25
30
|
>>> len(Q.example().option_permutations())
|
26
31
|
24
|
27
32
|
"""
|
@@ -39,66 +44,60 @@ class QuestionBaseGenMixin:
|
|
39
44
|
questions.append(question)
|
40
45
|
return questions
|
41
46
|
|
47
|
+
def draw(self) -> "QuestionBase":
|
48
|
+
"""Return a new question with a randomly selected permutation of the options.
|
49
|
+
|
50
|
+
If the question has no options, returns a copy of the original question.
|
51
|
+
|
52
|
+
>>> from edsl.questions.QuestionMultipleChoice import QuestionMultipleChoice as Q
|
53
|
+
>>> q = Q.example()
|
54
|
+
>>> drawn = q.draw()
|
55
|
+
>>> len(drawn.question_options) == len(q.question_options)
|
56
|
+
True
|
57
|
+
>>> q is drawn
|
58
|
+
False
|
59
|
+
"""
|
60
|
+
|
61
|
+
if not hasattr(self, "question_options"):
|
62
|
+
return copy.deepcopy(self)
|
63
|
+
|
64
|
+
import random
|
65
|
+
|
66
|
+
question = copy.deepcopy(self)
|
67
|
+
question.question_options = list(
|
68
|
+
random.sample(self.question_options, len(self.question_options))
|
69
|
+
)
|
70
|
+
return question
|
71
|
+
|
42
72
|
def loop(self, scenario_list: ScenarioList) -> List[QuestionBase]:
|
43
73
|
"""Return a list of questions with the question name modified for each scenario.
|
44
74
|
|
45
75
|
:param scenario_list: The list of scenarios to loop through.
|
46
76
|
|
47
|
-
>>> from edsl import QuestionFreeText
|
48
|
-
>>> from edsl import ScenarioList
|
77
|
+
>>> from edsl.questions.QuestionFreeText import QuestionFreeText
|
78
|
+
>>> from edsl.scenarios.ScenarioList import ScenarioList
|
49
79
|
>>> q = QuestionFreeText(question_text = "What are your thoughts on: {{ subject}}?", question_name = "base_{{subject}}")
|
50
80
|
>>> len(q.loop(ScenarioList.from_list("subject", ["Math", "Economics", "Chemistry"])))
|
51
81
|
3
|
52
|
-
|
53
82
|
"""
|
54
|
-
from
|
55
|
-
from edsl.questions.QuestionBase import QuestionBase
|
83
|
+
from edsl.questions.loop_processor import LoopProcessor
|
56
84
|
|
57
|
-
|
58
|
-
|
59
|
-
for index, scenario in enumerate(scenario_list):
|
60
|
-
env = Environment()
|
61
|
-
new_data = self.to_dict().copy()
|
62
|
-
for key, value in [(k, v) for k, v in new_data.items() if v is not None]:
|
63
|
-
if (
|
64
|
-
isinstance(value, str) or isinstance(value, int)
|
65
|
-
) and key != "question_options":
|
66
|
-
new_data[key] = env.from_string(value).render(scenario)
|
67
|
-
elif isinstance(value, list):
|
68
|
-
new_data[key] = [
|
69
|
-
env.from_string(v).render(scenario) if isinstance(v, str) else v
|
70
|
-
for v in value
|
71
|
-
]
|
72
|
-
elif isinstance(value, dict):
|
73
|
-
new_data[key] = {
|
74
|
-
(
|
75
|
-
env.from_string(k).render(scenario)
|
76
|
-
if isinstance(k, str)
|
77
|
-
else k
|
78
|
-
): (
|
79
|
-
env.from_string(v).render(scenario)
|
80
|
-
if isinstance(v, str)
|
81
|
-
else v
|
82
|
-
)
|
83
|
-
for k, v in value.items()
|
84
|
-
}
|
85
|
-
elif key == "question_options" and isinstance(value, str):
|
86
|
-
new_data[key] = value
|
87
|
-
else:
|
88
|
-
raise ValueError(
|
89
|
-
f"Unexpected value type: {type(value)} for key '{key}'"
|
90
|
-
)
|
85
|
+
lp = LoopProcessor(self)
|
86
|
+
return lp.process_templates(scenario_list)
|
91
87
|
|
92
|
-
|
93
|
-
|
88
|
+
def render(self, replacement_dict: dict) -> "QuestionBase":
|
89
|
+
"""Render the question components as jinja2 templates with the replacement dictionary.
|
94
90
|
|
95
|
-
|
96
|
-
return questions
|
91
|
+
:param replacement_dict: The dictionary of values to replace in the question components.
|
97
92
|
|
98
|
-
|
99
|
-
""
|
93
|
+
>>> from edsl.questions.QuestionFreeText import QuestionFreeText
|
94
|
+
>>> q = QuestionFreeText(question_name = "color", question_text = "What is your favorite {{ thing }}?")
|
95
|
+
>>> q.render({"thing": "color"})
|
96
|
+
Question('free_text', question_name = \"""color\""", question_text = \"""What is your favorite color?\""")
|
97
|
+
|
98
|
+
"""
|
100
99
|
from jinja2 import Environment
|
101
|
-
from edsl import Scenario
|
100
|
+
from edsl.scenarios.Scenario import Scenario
|
102
101
|
|
103
102
|
strings_only_replacement_dict = {
|
104
103
|
k: v for k, v in replacement_dict.items() if not isinstance(v, Scenario)
|
@@ -123,15 +122,23 @@ class QuestionBaseGenMixin:
|
|
123
122
|
|
124
123
|
return self.apply_function(render_string)
|
125
124
|
|
126
|
-
def apply_function(
|
125
|
+
def apply_function(
|
126
|
+
self, func: Callable, exclude_components: List[str] = None
|
127
|
+
) -> QuestionBase:
|
127
128
|
"""Apply a function to the question parts
|
128
129
|
|
130
|
+
:param func: The function to apply to the question parts.
|
131
|
+
:param exclude_components: The components to exclude from the function application.
|
132
|
+
|
129
133
|
>>> from edsl.questions import QuestionFreeText
|
130
134
|
>>> q = QuestionFreeText(question_name = "color", question_text = "What is your favorite color?")
|
131
135
|
>>> shouting = lambda x: x.upper()
|
132
136
|
>>> q.apply_function(shouting)
|
133
137
|
Question('free_text', question_name = \"""color\""", question_text = \"""WHAT IS YOUR FAVORITE COLOR?\""")
|
134
138
|
|
139
|
+
>>> q.apply_function(shouting, exclude_components = ["question_type"])
|
140
|
+
Question('free_text', question_name = \"""COLOR\""", question_text = \"""WHAT IS YOUR FAVORITE COLOR?\""")
|
141
|
+
|
135
142
|
"""
|
136
143
|
from edsl.questions.QuestionBase import QuestionBase
|
137
144
|
|
@@ -96,7 +96,7 @@ class Question(metaclass=Meta):
|
|
96
96
|
|
97
97
|
>>> from edsl import Question
|
98
98
|
>>> Question.list_question_types()
|
99
|
-
['checkbox', 'extract', 'free_text', 'functional', 'likert_five', 'linear_scale', 'list', 'multiple_choice', 'numerical', 'rank', 'top_k', 'yes_no']
|
99
|
+
['checkbox', 'extract', 'free_text', 'functional', 'likert_five', 'linear_scale', 'list', 'matrix', 'multiple_choice', 'numerical', 'rank', 'top_k', 'yes_no']
|
100
100
|
"""
|
101
101
|
return [
|
102
102
|
q
|
@@ -1,23 +1,22 @@
|
|
1
1
|
from abc import ABC, abstractmethod
|
2
|
-
from pydantic import BaseModel, Field, field_validator
|
3
|
-
|
4
|
-
# from decimal import Decimal
|
5
2
|
from typing import Optional, Any, List, TypedDict
|
6
3
|
|
7
|
-
from
|
8
|
-
from pydantic import ValidationError
|
4
|
+
from pydantic import BaseModel, Field, field_validator, ValidationError
|
9
5
|
|
6
|
+
from edsl.exceptions.questions import QuestionAnswerValidationError
|
7
|
+
from edsl.questions.ExceptionExplainer import ExceptionExplainer
|
10
8
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
9
|
+
from edsl.questions.data_structures import (
|
10
|
+
RawEdslAnswerDict,
|
11
|
+
EdslAnswerDict,
|
12
|
+
)
|
15
13
|
|
16
14
|
|
17
15
|
class ResponseValidatorABC(ABC):
|
18
16
|
required_params: List[str] = []
|
19
17
|
|
20
18
|
def __init_subclass__(cls, **kwargs):
|
19
|
+
"""This is a metaclass that ensures that all subclasses of ResponseValidatorABC have the required class variables."""
|
21
20
|
super().__init_subclass__(**kwargs)
|
22
21
|
required_class_vars = ["required_params", "valid_examples", "invalid_examples"]
|
23
22
|
for var in required_class_vars:
|
@@ -52,12 +51,7 @@ class ResponseValidatorABC(ABC):
|
|
52
51
|
if not hasattr(self, "permissive"):
|
53
52
|
self.permissive = False
|
54
53
|
|
55
|
-
self.fixes_tried = 0
|
56
|
-
|
57
|
-
class RawEdslAnswerDict(TypedDict):
|
58
|
-
answer: Any
|
59
|
-
comment: Optional[str]
|
60
|
-
generated_tokens: Optional[str]
|
54
|
+
self.fixes_tried = 0 # how many times we've tried to fix the answer
|
61
55
|
|
62
56
|
def _preprocess(self, data: RawEdslAnswerDict) -> RawEdslAnswerDict:
|
63
57
|
"""This is for testing purposes. A question can be given an exception to throw or an answer to always return.
|
@@ -72,7 +66,8 @@ class ResponseValidatorABC(ABC):
|
|
72
66
|
return self.override_answer if self.override_answer else data
|
73
67
|
|
74
68
|
def _base_validate(self, data: RawEdslAnswerDict) -> BaseModel:
|
75
|
-
"""This is the main validation function. It takes the response_model and checks the data against it,
|
69
|
+
"""This is the main validation function. It takes the response_model and checks the data against it,
|
70
|
+
returning the instantiated model.
|
76
71
|
|
77
72
|
>>> rv = ResponseValidatorABC.example("numerical")
|
78
73
|
>>> rv._base_validate({"answer": 42})
|
@@ -81,16 +76,13 @@ class ResponseValidatorABC(ABC):
|
|
81
76
|
try:
|
82
77
|
return self.response_model(**data)
|
83
78
|
except ValidationError as e:
|
84
|
-
raise QuestionAnswerValidationError(
|
79
|
+
raise QuestionAnswerValidationError(
|
80
|
+
message=str(e), pydantic_error=e, data=data, model=self.response_model
|
81
|
+
)
|
85
82
|
|
86
83
|
def post_validation_answer_convert(self, data):
|
87
84
|
return data
|
88
85
|
|
89
|
-
class EdslAnswerDict(TypedDict):
|
90
|
-
answer: Any
|
91
|
-
comment: Optional[str]
|
92
|
-
generated_tokens: Optional[str]
|
93
|
-
|
94
86
|
def validate(
|
95
87
|
self,
|
96
88
|
raw_edsl_answer_dict: RawEdslAnswerDict,
|
@@ -128,10 +120,12 @@ class ResponseValidatorABC(ABC):
|
|
128
120
|
edsl_answer_dict = self._extract_answer(pydantic_edsl_answer)
|
129
121
|
return self._post_process(edsl_answer_dict)
|
130
122
|
except QuestionAnswerValidationError as e:
|
131
|
-
if verbose:
|
132
|
-
print(f"Failed to validate {raw_edsl_answer_dict}; {str(e)}")
|
133
123
|
return self._handle_exception(e, raw_edsl_answer_dict)
|
134
124
|
|
125
|
+
def human_explanation(self, e: QuestionAnswerValidationError):
|
126
|
+
explanation = ExceptionExplainer(e, model_response=e.data).explain()
|
127
|
+
return explanation
|
128
|
+
|
135
129
|
def _handle_exception(self, e: Exception, raw_edsl_answer_dict) -> EdslAnswerDict:
|
136
130
|
if self.fixes_tried == 0:
|
137
131
|
self.original_exception = e
|
@@ -140,12 +134,18 @@ class ResponseValidatorABC(ABC):
|
|
140
134
|
self.fixes_tried += 1
|
141
135
|
fixed_data = self.fix(raw_edsl_answer_dict)
|
142
136
|
try:
|
143
|
-
return self.validate(fixed_data, fix=True)
|
137
|
+
return self.validate(fixed_data, fix=True) # early return if validates
|
144
138
|
except Exception as e:
|
145
139
|
pass # we don't log failed fixes
|
146
140
|
|
141
|
+
# If the exception is already a QuestionAnswerValidationError, raise it
|
142
|
+
if isinstance(self.original_exception, QuestionAnswerValidationError):
|
143
|
+
raise self.original_exception
|
144
|
+
|
145
|
+
# If nothing worked, raise the original exception
|
147
146
|
raise QuestionAnswerValidationError(
|
148
|
-
self.original_exception,
|
147
|
+
message=self.original_exception,
|
148
|
+
pydantic_error=self.original_exception,
|
149
149
|
data=raw_edsl_answer_dict,
|
150
150
|
model=self.response_model,
|
151
151
|
)
|
@@ -167,8 +167,22 @@ class ResponseValidatorABC(ABC):
|
|
167
167
|
return q.response_validator
|
168
168
|
|
169
169
|
|
170
|
+
def main():
|
171
|
+
rv = ResponseValidatorABC.example()
|
172
|
+
print(rv.validate({"answer": 42}))
|
173
|
+
|
174
|
+
|
170
175
|
# Example usage
|
171
176
|
if __name__ == "__main__":
|
172
177
|
import doctest
|
173
178
|
|
174
179
|
doctest.testmod(optionflags=doctest.ELLIPSIS)
|
180
|
+
|
181
|
+
rv = ResponseValidatorABC.example()
|
182
|
+
# print(rv.validate({"answer": 42}))
|
183
|
+
|
184
|
+
rv = ResponseValidatorABC.example()
|
185
|
+
try:
|
186
|
+
rv.validate({"answer": "120"})
|
187
|
+
except QuestionAnswerValidationError as e:
|
188
|
+
print(rv.human_explanation(e))
|
@@ -0,0 +1,34 @@
|
|
1
|
+
from edsl.questions.data_structures import BaseModel
|
2
|
+
from edsl.questions.response_validator_abc import ResponseValidatorABC
|
3
|
+
|
4
|
+
|
5
|
+
class ResponseValidatorFactory:
|
6
|
+
"""Factory class to create a response validator for a question."""
|
7
|
+
|
8
|
+
def __init__(self, question):
|
9
|
+
self.question = question
|
10
|
+
|
11
|
+
@property
|
12
|
+
def response_model(self) -> type["BaseModel"]:
|
13
|
+
if self.question._response_model is not None:
|
14
|
+
return self.question._response_model
|
15
|
+
else:
|
16
|
+
return self.question.create_response_model()
|
17
|
+
|
18
|
+
@property
|
19
|
+
def response_validator(self) -> "ResponseValidatorABC":
|
20
|
+
"""Return the response validator."""
|
21
|
+
params = (
|
22
|
+
{
|
23
|
+
"response_model": self.question.response_model,
|
24
|
+
}
|
25
|
+
| {k: getattr(self.question, k) for k in self.validator_parameters}
|
26
|
+
| {"exception_to_throw": getattr(self.question, "exception_to_throw", None)}
|
27
|
+
| {"override_answer": getattr(self.question, "override_answer", None)}
|
28
|
+
)
|
29
|
+
return self.question.response_validator_class(**params)
|
30
|
+
|
31
|
+
@property
|
32
|
+
def validator_parameters(self) -> list[str]:
|
33
|
+
"""Return the parameters required for the response validator."""
|
34
|
+
return self.question.response_validator_class.required_params
|
@@ -0,0 +1 @@
|
|
1
|
+
|
@@ -0,0 +1,20 @@
|
|
1
|
+
{{question_text}}
|
2
|
+
|
3
|
+
Rows:
|
4
|
+
{% for item in question_items %}
|
5
|
+
{{ loop.index0 }}: {{item}}
|
6
|
+
{% endfor %}
|
7
|
+
|
8
|
+
Columns:
|
9
|
+
{% for option in question_options %}
|
10
|
+
{{ loop.index0 }}: {{option}}
|
11
|
+
{%- if option in option_labels %}
|
12
|
+
({{option_labels[option]}})
|
13
|
+
{%- endif %}
|
14
|
+
{% endfor %}
|
15
|
+
|
16
|
+
|
17
|
+
Select one column option for each row.
|
18
|
+
{% if required %}
|
19
|
+
All rows require a response.
|
20
|
+
{% endif %}
|
edsl/results/CSSParameterizer.py
CHANGED
@@ -67,7 +67,7 @@ class CSSParameterizer:
|
|
67
67
|
missing_vars = self._validate_parameters(parameters)
|
68
68
|
|
69
69
|
if missing_vars:
|
70
|
-
print(f"Error: Missing required variables: {missing_vars}")
|
70
|
+
# print(f"Error: Missing required variables: {missing_vars}")
|
71
71
|
return None
|
72
72
|
|
73
73
|
# Format parameters with -- prefix if not present
|