edsl 0.1.39__py3-none-any.whl → 0.1.39.dev1__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 +116 -197
- edsl/__init__.py +7 -15
- edsl/__version__.py +1 -1
- edsl/agents/Agent.py +147 -351
- edsl/agents/AgentList.py +73 -211
- edsl/agents/Invigilator.py +50 -101
- edsl/agents/InvigilatorBase.py +70 -62
- edsl/agents/PromptConstructor.py +225 -143
- edsl/agents/__init__.py +1 -0
- edsl/agents/prompt_helpers.py +3 -3
- edsl/auto/AutoStudy.py +5 -18
- edsl/auto/StageBase.py +40 -53
- edsl/auto/StageQuestions.py +1 -2
- edsl/auto/utilities.py +6 -0
- edsl/config.py +2 -22
- edsl/conversation/car_buying.py +1 -2
- edsl/coop/PriceFetcher.py +1 -1
- edsl/coop/coop.py +47 -125
- edsl/coop/utils.py +14 -14
- edsl/data/Cache.py +27 -45
- edsl/data/CacheEntry.py +15 -12
- edsl/data/CacheHandler.py +12 -31
- edsl/data/RemoteCacheSync.py +46 -154
- edsl/data/__init__.py +3 -4
- edsl/data_transfer_models.py +1 -2
- edsl/enums.py +0 -27
- edsl/exceptions/__init__.py +50 -50
- edsl/exceptions/agents.py +0 -12
- edsl/exceptions/questions.py +6 -24
- edsl/exceptions/scenarios.py +0 -7
- edsl/inference_services/AnthropicService.py +19 -38
- edsl/inference_services/AwsBedrock.py +2 -0
- edsl/inference_services/AzureAI.py +2 -0
- edsl/inference_services/GoogleService.py +12 -7
- edsl/inference_services/InferenceServiceABC.py +85 -18
- edsl/inference_services/InferenceServicesCollection.py +79 -120
- edsl/inference_services/MistralAIService.py +3 -0
- edsl/inference_services/OpenAIService.py +35 -47
- edsl/inference_services/PerplexityService.py +3 -0
- edsl/inference_services/TestService.py +10 -11
- edsl/inference_services/TogetherAIService.py +3 -5
- edsl/jobs/Answers.py +14 -1
- edsl/jobs/Jobs.py +431 -356
- edsl/jobs/JobsChecks.py +10 -35
- edsl/jobs/JobsPrompts.py +4 -6
- edsl/jobs/JobsRemoteInferenceHandler.py +133 -205
- edsl/jobs/buckets/BucketCollection.py +3 -44
- edsl/jobs/buckets/TokenBucket.py +21 -53
- edsl/jobs/interviews/Interview.py +408 -143
- edsl/jobs/runners/JobsRunnerAsyncio.py +403 -88
- edsl/jobs/runners/JobsRunnerStatus.py +165 -133
- edsl/jobs/tasks/QuestionTaskCreator.py +19 -21
- edsl/jobs/tasks/TaskHistory.py +18 -38
- edsl/jobs/tasks/task_status_enum.py +2 -0
- edsl/language_models/KeyLookup.py +30 -0
- edsl/language_models/LanguageModel.py +236 -194
- edsl/language_models/ModelList.py +19 -28
- edsl/language_models/__init__.py +2 -1
- edsl/language_models/registry.py +190 -0
- edsl/language_models/repair.py +2 -2
- edsl/language_models/unused/ReplicateBase.py +83 -0
- edsl/language_models/utilities.py +4 -5
- edsl/notebooks/Notebook.py +14 -19
- edsl/prompts/Prompt.py +39 -29
- edsl/questions/{answer_validator_mixin.py → AnswerValidatorMixin.py} +2 -47
- edsl/questions/QuestionBase.py +214 -68
- edsl/questions/{question_base_gen_mixin.py → QuestionBaseGenMixin.py} +50 -57
- edsl/questions/QuestionBasePromptsMixin.py +3 -7
- edsl/questions/QuestionBudget.py +1 -1
- edsl/questions/QuestionCheckBox.py +3 -3
- edsl/questions/QuestionExtract.py +7 -5
- edsl/questions/QuestionFreeText.py +3 -2
- edsl/questions/QuestionList.py +18 -10
- edsl/questions/QuestionMultipleChoice.py +23 -67
- edsl/questions/QuestionNumerical.py +4 -2
- edsl/questions/QuestionRank.py +17 -7
- edsl/questions/{response_validator_abc.py → ResponseValidatorABC.py} +26 -40
- edsl/questions/SimpleAskMixin.py +3 -4
- edsl/questions/__init__.py +1 -2
- edsl/questions/derived/QuestionLinearScale.py +3 -6
- edsl/questions/derived/QuestionTopK.py +1 -1
- edsl/questions/descriptors.py +3 -17
- edsl/questions/question_registry.py +1 -1
- edsl/results/CSSParameterizer.py +1 -1
- edsl/results/Dataset.py +7 -170
- edsl/results/DatasetExportMixin.py +305 -168
- edsl/results/DatasetTree.py +8 -28
- edsl/results/Result.py +206 -298
- edsl/results/Results.py +131 -149
- edsl/results/ResultsDBMixin.py +238 -0
- edsl/results/ResultsExportMixin.py +0 -2
- edsl/results/{results_selector.py → Selector.py} +13 -23
- edsl/results/TableDisplay.py +171 -98
- edsl/results/__init__.py +1 -1
- edsl/scenarios/FileStore.py +239 -150
- edsl/scenarios/Scenario.py +193 -90
- edsl/scenarios/ScenarioHtmlMixin.py +3 -4
- edsl/scenarios/{scenario_join.py → ScenarioJoin.py} +6 -10
- edsl/scenarios/ScenarioList.py +244 -415
- edsl/scenarios/ScenarioListExportMixin.py +7 -0
- edsl/scenarios/ScenarioListPdfMixin.py +37 -15
- edsl/scenarios/__init__.py +2 -1
- edsl/study/ObjectEntry.py +1 -1
- edsl/study/SnapShot.py +1 -1
- edsl/study/Study.py +12 -5
- edsl/surveys/Rule.py +4 -5
- edsl/surveys/RuleCollection.py +27 -25
- edsl/surveys/Survey.py +791 -270
- edsl/surveys/SurveyCSS.py +8 -20
- edsl/surveys/{SurveyFlowVisualization.py → SurveyFlowVisualizationMixin.py} +9 -11
- edsl/surveys/__init__.py +2 -4
- edsl/surveys/descriptors.py +2 -6
- edsl/surveys/instructions/ChangeInstruction.py +2 -1
- edsl/surveys/instructions/Instruction.py +13 -4
- edsl/surveys/instructions/InstructionCollection.py +6 -11
- 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/utilities.py +23 -35
- {edsl-0.1.39.dist-info → edsl-0.1.39.dev1.dist-info}/METADATA +10 -12
- edsl-0.1.39.dev1.dist-info/RECORD +277 -0
- {edsl-0.1.39.dist-info → edsl-0.1.39.dev1.dist-info}/WHEEL +1 -1
- edsl/agents/QuestionInstructionPromptBuilder.py +0 -128
- edsl/agents/QuestionTemplateReplacementsBuilder.py +0 -137
- edsl/agents/question_option_processor.py +0 -172
- edsl/coop/CoopFunctionsMixin.py +0 -15
- edsl/coop/ExpectedParrotKeyHandler.py +0 -125
- edsl/exceptions/inference_services.py +0 -5
- edsl/inference_services/AvailableModelCacheHandler.py +0 -184
- edsl/inference_services/AvailableModelFetcher.py +0 -215
- edsl/inference_services/ServiceAvailability.py +0 -135
- edsl/inference_services/data_structures.py +0 -134
- edsl/jobs/AnswerQuestionFunctionConstructor.py +0 -223
- edsl/jobs/FetchInvigilator.py +0 -47
- edsl/jobs/InterviewTaskManager.py +0 -98
- edsl/jobs/InterviewsConstructor.py +0 -50
- edsl/jobs/JobsComponentConstructor.py +0 -189
- edsl/jobs/JobsRemoteInferenceLogger.py +0 -239
- edsl/jobs/RequestTokenEstimator.py +0 -30
- edsl/jobs/async_interview_runner.py +0 -138
- edsl/jobs/buckets/TokenBucketAPI.py +0 -211
- edsl/jobs/buckets/TokenBucketClient.py +0 -191
- edsl/jobs/check_survey_scenario_compatibility.py +0 -85
- edsl/jobs/data_structures.py +0 -120
- edsl/jobs/decorators.py +0 -35
- edsl/jobs/jobs_status_enums.py +0 -9
- edsl/jobs/loggers/HTMLTableJobLogger.py +0 -304
- edsl/jobs/results_exceptions_handler.py +0 -98
- edsl/language_models/ComputeCost.py +0 -63
- edsl/language_models/PriceManager.py +0 -127
- edsl/language_models/RawResponseHandler.py +0 -106
- edsl/language_models/ServiceDataSources.py +0 -0
- edsl/language_models/key_management/KeyLookup.py +0 -63
- edsl/language_models/key_management/KeyLookupBuilder.py +0 -273
- edsl/language_models/key_management/KeyLookupCollection.py +0 -38
- edsl/language_models/key_management/__init__.py +0 -0
- edsl/language_models/key_management/models.py +0 -131
- edsl/language_models/model.py +0 -256
- edsl/notebooks/NotebookToLaTeX.py +0 -142
- edsl/questions/ExceptionExplainer.py +0 -77
- edsl/questions/HTMLQuestion.py +0 -103
- edsl/questions/QuestionMatrix.py +0 -265
- edsl/questions/data_structures.py +0 -20
- edsl/questions/loop_processor.py +0 -149
- edsl/questions/response_validator_factory.py +0 -34
- edsl/questions/templates/matrix/__init__.py +0 -1
- edsl/questions/templates/matrix/answering_instructions.jinja +0 -5
- edsl/questions/templates/matrix/question_presentation.jinja +0 -20
- edsl/results/MarkdownToDocx.py +0 -122
- edsl/results/MarkdownToPDF.py +0 -111
- edsl/results/TextEditor.py +0 -50
- edsl/results/file_exports.py +0 -252
- edsl/results/smart_objects.py +0 -96
- edsl/results/table_data_class.py +0 -12
- edsl/results/table_renderers.py +0 -118
- edsl/scenarios/ConstructDownloadLink.py +0 -109
- edsl/scenarios/DocumentChunker.py +0 -102
- edsl/scenarios/DocxScenario.py +0 -16
- edsl/scenarios/PdfExtractor.py +0 -40
- edsl/scenarios/directory_scanner.py +0 -96
- edsl/scenarios/file_methods.py +0 -85
- edsl/scenarios/handlers/__init__.py +0 -13
- edsl/scenarios/handlers/csv.py +0 -49
- edsl/scenarios/handlers/docx.py +0 -76
- edsl/scenarios/handlers/html.py +0 -37
- edsl/scenarios/handlers/json.py +0 -111
- edsl/scenarios/handlers/latex.py +0 -5
- edsl/scenarios/handlers/md.py +0 -51
- edsl/scenarios/handlers/pdf.py +0 -68
- edsl/scenarios/handlers/png.py +0 -39
- edsl/scenarios/handlers/pptx.py +0 -105
- edsl/scenarios/handlers/py.py +0 -294
- edsl/scenarios/handlers/sql.py +0 -313
- edsl/scenarios/handlers/sqlite.py +0 -149
- edsl/scenarios/handlers/txt.py +0 -33
- edsl/scenarios/scenario_selector.py +0 -156
- edsl/surveys/ConstructDAG.py +0 -92
- edsl/surveys/EditSurvey.py +0 -221
- edsl/surveys/InstructionHandler.py +0 -100
- edsl/surveys/MemoryManagement.py +0 -72
- edsl/surveys/RuleManager.py +0 -172
- edsl/surveys/Simulator.py +0 -75
- edsl/surveys/SurveyToApp.py +0 -141
- edsl/utilities/PrettyList.py +0 -56
- edsl/utilities/is_notebook.py +0 -18
- edsl/utilities/is_valid_variable_name.py +0 -11
- edsl/utilities/remove_edsl_version.py +0 -24
- edsl-0.1.39.dist-info/RECORD +0 -358
- /edsl/questions/{register_questions_meta.py → RegisterQuestionsMeta.py} +0 -0
- /edsl/results/{results_fetch_mixin.py → ResultsFetchMixin.py} +0 -0
- /edsl/results/{results_tools_mixin.py → ResultsToolsMixin.py} +0 -0
- {edsl-0.1.39.dist-info → edsl-0.1.39.dev1.dist-info}/LICENSE +0 -0
edsl/questions/HTMLQuestion.py
DELETED
@@ -1,103 +0,0 @@
|
|
1
|
-
from typing import Optional
|
2
|
-
from edsl.prompts.Prompt import Prompt
|
3
|
-
|
4
|
-
|
5
|
-
class HTMLQuestion:
|
6
|
-
def __init__(self, question):
|
7
|
-
self.question = question
|
8
|
-
|
9
|
-
def html(
|
10
|
-
self,
|
11
|
-
scenario: Optional[dict] = None,
|
12
|
-
agent: Optional[dict] = {},
|
13
|
-
answers: Optional[dict] = None,
|
14
|
-
include_question_name: bool = False,
|
15
|
-
height: Optional[int] = None,
|
16
|
-
width: Optional[int] = None,
|
17
|
-
iframe=False,
|
18
|
-
):
|
19
|
-
"""Return the question in HTML format."""
|
20
|
-
from jinja2 import Template
|
21
|
-
|
22
|
-
if scenario is None:
|
23
|
-
scenario = {}
|
24
|
-
|
25
|
-
prior_answers_dict = {}
|
26
|
-
|
27
|
-
if isinstance(answers, dict):
|
28
|
-
for key, value in answers.items():
|
29
|
-
if not key.endswith("_comment") and not key.endswith(
|
30
|
-
"_generated_tokens"
|
31
|
-
):
|
32
|
-
prior_answers_dict[key] = {"answer": value}
|
33
|
-
|
34
|
-
base_template = """
|
35
|
-
<div id="{{ question_name }}" class="survey_question" data-type="{{ question_type }}">
|
36
|
-
{% if include_question_name %}
|
37
|
-
<p>question_name: {{ question_name }}</p>
|
38
|
-
{% endif %}
|
39
|
-
<p class="question_text">{{ question_text }}</p>
|
40
|
-
{{ question_content }}
|
41
|
-
</div>
|
42
|
-
"""
|
43
|
-
if not hasattr(self.question, "question_type"):
|
44
|
-
self.question.question_type = "unknown"
|
45
|
-
|
46
|
-
if hasattr(self.question, "question_html_content"):
|
47
|
-
question_content = self.question.question_html_content
|
48
|
-
else:
|
49
|
-
question_content = Template("")
|
50
|
-
|
51
|
-
base_template = Template(base_template)
|
52
|
-
|
53
|
-
context = {
|
54
|
-
"scenario": scenario,
|
55
|
-
"agent": agent,
|
56
|
-
} | prior_answers_dict
|
57
|
-
|
58
|
-
# Render the question text
|
59
|
-
try:
|
60
|
-
question_text = Template(self.question.question_text).render(context)
|
61
|
-
except Exception as e:
|
62
|
-
print(
|
63
|
-
f"Error rendering question: question_text = {self.question.question_text}, error = {e}"
|
64
|
-
)
|
65
|
-
question_text = self.question.question_text
|
66
|
-
|
67
|
-
try:
|
68
|
-
question_content = Template(question_content).render(context)
|
69
|
-
except Exception as e:
|
70
|
-
print(
|
71
|
-
f"Error rendering question: question_content = {question_content}, error = {e}"
|
72
|
-
)
|
73
|
-
question_content = question_content
|
74
|
-
|
75
|
-
try:
|
76
|
-
params = {
|
77
|
-
"question_name": self.question.question_name,
|
78
|
-
"question_text": question_text,
|
79
|
-
"question_type": self.question.question_type,
|
80
|
-
"question_content": question_content,
|
81
|
-
"include_question_name": include_question_name,
|
82
|
-
}
|
83
|
-
except Exception as e:
|
84
|
-
raise ValueError(
|
85
|
-
f"Error rendering question: params = {params}, error = {e}"
|
86
|
-
)
|
87
|
-
rendered_html = base_template.render(**params)
|
88
|
-
|
89
|
-
if iframe:
|
90
|
-
import html
|
91
|
-
from IPython.display import display, HTML
|
92
|
-
|
93
|
-
height = height or 200
|
94
|
-
width = width or 600
|
95
|
-
escaped_output = html.escape(rendered_html)
|
96
|
-
# escaped_output = rendered_html
|
97
|
-
iframe = f""""
|
98
|
-
<iframe srcdoc="{ escaped_output }" style="width: {width}px; height: {height}px;"></iframe>
|
99
|
-
"""
|
100
|
-
display(HTML(iframe))
|
101
|
-
return None
|
102
|
-
|
103
|
-
return rendered_html
|
edsl/questions/QuestionMatrix.py
DELETED
@@ -1,265 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
from typing import Union, Optional, Dict, List, Any
|
3
|
-
|
4
|
-
from pydantic import BaseModel, Field, field_validator
|
5
|
-
from jinja2 import Template
|
6
|
-
import random
|
7
|
-
from edsl.questions.QuestionBase import QuestionBase
|
8
|
-
from edsl.questions.descriptors import (
|
9
|
-
QuestionOptionsDescriptor,
|
10
|
-
OptionLabelDescriptor,
|
11
|
-
QuestionTextDescriptor,
|
12
|
-
)
|
13
|
-
from edsl.questions.response_validator_abc import ResponseValidatorABC
|
14
|
-
from edsl.questions.decorators import inject_exception
|
15
|
-
from edsl.exceptions.questions import (
|
16
|
-
QuestionAnswerValidationError,
|
17
|
-
QuestionCreationValidationError,
|
18
|
-
)
|
19
|
-
|
20
|
-
|
21
|
-
def create_matrix_response(
|
22
|
-
question_items: List[str],
|
23
|
-
question_options: List[Union[int, str, float]],
|
24
|
-
permissive: bool = False,
|
25
|
-
):
|
26
|
-
"""Create a response model for matrix questions.
|
27
|
-
|
28
|
-
The response model validates that:
|
29
|
-
1. All question items are answered
|
30
|
-
2. Each answer is from the allowed options
|
31
|
-
"""
|
32
|
-
|
33
|
-
if permissive:
|
34
|
-
|
35
|
-
class MatrixResponse(BaseModel):
|
36
|
-
answer: Dict[str, Any]
|
37
|
-
comment: Optional[str] = None
|
38
|
-
generated_tokens: Optional[Any] = None
|
39
|
-
|
40
|
-
else:
|
41
|
-
|
42
|
-
class MatrixResponse(BaseModel):
|
43
|
-
answer: Dict[str, Union[int, str, float]] = Field(
|
44
|
-
..., description="Mapping of items to selected options"
|
45
|
-
)
|
46
|
-
comment: Optional[str] = None
|
47
|
-
generated_tokens: Optional[Any] = None
|
48
|
-
|
49
|
-
@field_validator("answer")
|
50
|
-
def validate_answer(cls, v, values, **kwargs):
|
51
|
-
# Check that all items have responses
|
52
|
-
if not all(item in v for item in question_items):
|
53
|
-
missing = set(question_items) - set(v.keys())
|
54
|
-
raise ValueError(f"Missing responses for items: {missing}")
|
55
|
-
|
56
|
-
# Check that all responses are valid options
|
57
|
-
if not all(answer in question_options for answer in v.values()):
|
58
|
-
invalid = [ans for ans in v.values() if ans not in question_options]
|
59
|
-
raise ValueError(f"Invalid options selected: {invalid}")
|
60
|
-
return v
|
61
|
-
|
62
|
-
return MatrixResponse
|
63
|
-
|
64
|
-
|
65
|
-
class MatrixResponseValidator(ResponseValidatorABC):
|
66
|
-
required_params = ["question_items", "question_options", "permissive"]
|
67
|
-
|
68
|
-
valid_examples = [
|
69
|
-
(
|
70
|
-
{"answer": {"Item1": 1, "Item2": 2}},
|
71
|
-
{
|
72
|
-
"question_items": ["Item1", "Item2"],
|
73
|
-
"question_options": [1, 2, 3],
|
74
|
-
},
|
75
|
-
)
|
76
|
-
]
|
77
|
-
|
78
|
-
invalid_examples = [
|
79
|
-
(
|
80
|
-
{"answer": {"Item1": 1}},
|
81
|
-
{
|
82
|
-
"question_items": ["Item1", "Item2"],
|
83
|
-
"question_options": [1, 2, 3],
|
84
|
-
},
|
85
|
-
"Missing responses for some items",
|
86
|
-
),
|
87
|
-
(
|
88
|
-
{"answer": {"Item1": 4, "Item2": 5}},
|
89
|
-
{
|
90
|
-
"question_items": ["Item1", "Item2"],
|
91
|
-
"question_options": [1, 2, 3],
|
92
|
-
},
|
93
|
-
"Invalid options selected",
|
94
|
-
),
|
95
|
-
]
|
96
|
-
|
97
|
-
def fix(self, response, verbose=False):
|
98
|
-
if verbose:
|
99
|
-
print(f"Fixing matrix response: {response}")
|
100
|
-
|
101
|
-
# If we have generated tokens, try to parse them
|
102
|
-
if "generated_tokens" in response:
|
103
|
-
try:
|
104
|
-
import json
|
105
|
-
|
106
|
-
fixed = json.loads(response["generated_tokens"])
|
107
|
-
if isinstance(fixed, dict):
|
108
|
-
# Map numeric keys to question items
|
109
|
-
mapped_answer = {}
|
110
|
-
for idx, item in enumerate(self.question_items):
|
111
|
-
if str(idx) in fixed:
|
112
|
-
mapped_answer[item] = fixed[str(idx)]
|
113
|
-
if (
|
114
|
-
mapped_answer
|
115
|
-
): # Only return if we successfully mapped some answers
|
116
|
-
return {"answer": mapped_answer}
|
117
|
-
except:
|
118
|
-
pass
|
119
|
-
|
120
|
-
# If answer uses numeric keys, map them to question items
|
121
|
-
if "answer" in response and isinstance(response["answer"], dict):
|
122
|
-
if all(str(key).isdigit() for key in response["answer"].keys()):
|
123
|
-
mapped_answer = {}
|
124
|
-
for idx, item in enumerate(self.question_items):
|
125
|
-
if str(idx) in response["answer"]:
|
126
|
-
mapped_answer[item] = response["answer"][str(idx)]
|
127
|
-
if mapped_answer: # Only update if we successfully mapped some answers
|
128
|
-
response["answer"] = mapped_answer
|
129
|
-
|
130
|
-
return response
|
131
|
-
|
132
|
-
|
133
|
-
class QuestionMatrix(QuestionBase):
|
134
|
-
"""A question that presents a matrix/grid where multiple items are rated using the same scale."""
|
135
|
-
|
136
|
-
question_type = "matrix"
|
137
|
-
question_text: str = QuestionTextDescriptor()
|
138
|
-
question_items: List[str] = QuestionOptionsDescriptor()
|
139
|
-
question_options: List[Union[int, str, float]] = QuestionOptionsDescriptor()
|
140
|
-
option_labels: Optional[Dict[Union[int, str, float], str]] = OptionLabelDescriptor()
|
141
|
-
|
142
|
-
_response_model = None
|
143
|
-
response_validator_class = MatrixResponseValidator
|
144
|
-
|
145
|
-
def __init__(
|
146
|
-
self,
|
147
|
-
question_name: str,
|
148
|
-
question_text: str,
|
149
|
-
question_items: List[str],
|
150
|
-
question_options: List[Union[int, str, float]],
|
151
|
-
option_labels: Optional[Dict[Union[int, str, float], str]] = None,
|
152
|
-
include_comment: bool = True,
|
153
|
-
answering_instructions: Optional[str] = None,
|
154
|
-
question_presentation: Optional[str] = None,
|
155
|
-
permissive: bool = False,
|
156
|
-
):
|
157
|
-
"""Initialize a matrix question.
|
158
|
-
|
159
|
-
Args:
|
160
|
-
question_name: The name of the question
|
161
|
-
question_text: The text of the question
|
162
|
-
question_items: List of items to be rated
|
163
|
-
question_options: List of rating options
|
164
|
-
option_labels: Optional mapping of options to their labels
|
165
|
-
include_comment: Whether to include a comment field
|
166
|
-
answering_instructions: Optional custom instructions
|
167
|
-
question_presentation: Optional custom presentation
|
168
|
-
permissive: Whether to strictly validate responses
|
169
|
-
"""
|
170
|
-
self.question_name = question_name
|
171
|
-
|
172
|
-
try:
|
173
|
-
self.question_text = question_text
|
174
|
-
except Exception as e:
|
175
|
-
raise QuestionCreationValidationError(
|
176
|
-
"question_text cannot be empty or too short!"
|
177
|
-
) from e
|
178
|
-
|
179
|
-
self.question_items = question_items
|
180
|
-
self.question_options = question_options
|
181
|
-
self.option_labels = option_labels or {}
|
182
|
-
|
183
|
-
self.include_comment = include_comment
|
184
|
-
self.answering_instructions = answering_instructions
|
185
|
-
self.question_presentation = question_presentation
|
186
|
-
self.permissive = permissive
|
187
|
-
|
188
|
-
def create_response_model(self):
|
189
|
-
return create_matrix_response(
|
190
|
-
self.question_items, self.question_options, self.permissive
|
191
|
-
)
|
192
|
-
|
193
|
-
@property
|
194
|
-
def question_html_content(self) -> str:
|
195
|
-
"""Generate HTML representation of the matrix question."""
|
196
|
-
template = Template(
|
197
|
-
"""
|
198
|
-
<table class="matrix-question">
|
199
|
-
<tr>
|
200
|
-
<th></th>
|
201
|
-
{% for option in question_options %}
|
202
|
-
<th>
|
203
|
-
{{ option }}
|
204
|
-
{% if option in option_labels %}
|
205
|
-
<br>
|
206
|
-
<small>{{ option_labels[option] }}</small>
|
207
|
-
{% endif %}
|
208
|
-
</th>
|
209
|
-
{% endfor %}
|
210
|
-
</tr>
|
211
|
-
{% for item in question_items %}
|
212
|
-
<tr>
|
213
|
-
<td>{{ item }}</td>
|
214
|
-
{% for option in question_options %}
|
215
|
-
<td>
|
216
|
-
<input type="radio"
|
217
|
-
name="{{ question_name }}_{{ item }}"
|
218
|
-
value="{{ option }}"
|
219
|
-
id="{{ question_name }}_{{ item }}_{{ option }}">
|
220
|
-
</td>
|
221
|
-
{% endfor %}
|
222
|
-
</tr>
|
223
|
-
{% endfor %}
|
224
|
-
</table>
|
225
|
-
"""
|
226
|
-
)
|
227
|
-
|
228
|
-
return template.render(
|
229
|
-
question_name=self.question_name,
|
230
|
-
question_items=self.question_items,
|
231
|
-
question_options=self.question_options,
|
232
|
-
option_labels=self.option_labels,
|
233
|
-
)
|
234
|
-
|
235
|
-
@classmethod
|
236
|
-
@inject_exception
|
237
|
-
def example(cls) -> QuestionMatrix:
|
238
|
-
"""Return an example matrix question."""
|
239
|
-
return cls(
|
240
|
-
question_name="child_happiness",
|
241
|
-
question_text="How happy would you be with different numbers of children?",
|
242
|
-
question_items=[
|
243
|
-
"No children",
|
244
|
-
"1 child",
|
245
|
-
"2 children",
|
246
|
-
"3 or more children",
|
247
|
-
],
|
248
|
-
question_options=[1, 2, 3, 4, 5],
|
249
|
-
option_labels={1: "Very sad", 3: "Neutral", 5: "Extremely happy"},
|
250
|
-
)
|
251
|
-
|
252
|
-
def _simulate_answer(self) -> dict:
|
253
|
-
"""Simulate a random valid answer."""
|
254
|
-
return {
|
255
|
-
"answer": {
|
256
|
-
item: random.choice(self.question_options)
|
257
|
-
for item in self.question_items
|
258
|
-
}
|
259
|
-
}
|
260
|
-
|
261
|
-
|
262
|
-
if __name__ == "__main__":
|
263
|
-
import doctest
|
264
|
-
|
265
|
-
doctest.testmod(optionflags=doctest.ELLIPSIS)
|
@@ -1,20 +0,0 @@
|
|
1
|
-
from typing import Any, Optional, TypedDict
|
2
|
-
from pydantic import BaseModel
|
3
|
-
|
4
|
-
|
5
|
-
class RawEdslAnswerDict(TypedDict):
|
6
|
-
answer: Any
|
7
|
-
comment: Optional[str]
|
8
|
-
generated_tokens: Optional[str]
|
9
|
-
|
10
|
-
|
11
|
-
class BaseResponse(BaseModel):
|
12
|
-
answer: Any
|
13
|
-
comment: Optional[str] = None
|
14
|
-
generated_tokens: Optional[str] = None
|
15
|
-
|
16
|
-
|
17
|
-
class EdslAnswerDict(TypedDict):
|
18
|
-
answer: Any
|
19
|
-
comment: Optional[str]
|
20
|
-
generated_tokens: Optional[str]
|
edsl/questions/loop_processor.py
DELETED
@@ -1,149 +0,0 @@
|
|
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,34 +0,0 @@
|
|
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
|
@@ -1 +0,0 @@
|
|
1
|
-
|
@@ -1,20 +0,0 @@
|
|
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 %}
|