edsl 0.1.32__py3-none-any.whl → 0.1.33__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 +9 -3
- edsl/TemplateLoader.py +24 -0
- edsl/__init__.py +8 -3
- edsl/__version__.py +1 -1
- edsl/agents/Agent.py +40 -8
- edsl/agents/AgentList.py +43 -0
- edsl/agents/Invigilator.py +135 -219
- edsl/agents/InvigilatorBase.py +148 -59
- edsl/agents/{PromptConstructionMixin.py → PromptConstructor.py} +138 -89
- edsl/agents/__init__.py +1 -0
- edsl/auto/AutoStudy.py +117 -0
- edsl/auto/StageBase.py +230 -0
- edsl/auto/StageGenerateSurvey.py +178 -0
- edsl/auto/StageLabelQuestions.py +125 -0
- edsl/auto/StagePersona.py +61 -0
- edsl/auto/StagePersonaDimensionValueRanges.py +88 -0
- edsl/auto/StagePersonaDimensionValues.py +74 -0
- edsl/auto/StagePersonaDimensions.py +69 -0
- edsl/auto/StageQuestions.py +73 -0
- edsl/auto/SurveyCreatorPipeline.py +21 -0
- edsl/auto/utilities.py +224 -0
- edsl/config.py +47 -56
- edsl/coop/PriceFetcher.py +58 -0
- edsl/coop/coop.py +50 -7
- edsl/data/Cache.py +35 -1
- edsl/data_transfer_models.py +73 -38
- edsl/enums.py +4 -0
- edsl/exceptions/language_models.py +25 -1
- edsl/exceptions/questions.py +62 -5
- edsl/exceptions/results.py +4 -0
- edsl/inference_services/AnthropicService.py +13 -11
- edsl/inference_services/AwsBedrock.py +19 -17
- edsl/inference_services/AzureAI.py +37 -20
- edsl/inference_services/GoogleService.py +16 -12
- edsl/inference_services/GroqService.py +2 -0
- edsl/inference_services/InferenceServiceABC.py +58 -3
- edsl/inference_services/MistralAIService.py +120 -0
- edsl/inference_services/OpenAIService.py +48 -54
- edsl/inference_services/TestService.py +80 -0
- edsl/inference_services/TogetherAIService.py +170 -0
- edsl/inference_services/models_available_cache.py +0 -6
- edsl/inference_services/registry.py +6 -0
- edsl/jobs/Answers.py +10 -12
- edsl/jobs/FailedQuestion.py +78 -0
- edsl/jobs/Jobs.py +37 -22
- edsl/jobs/buckets/BucketCollection.py +24 -15
- edsl/jobs/buckets/TokenBucket.py +93 -14
- edsl/jobs/interviews/Interview.py +366 -78
- edsl/jobs/interviews/{interview_exception_tracking.py → InterviewExceptionCollection.py} +14 -68
- edsl/jobs/interviews/InterviewExceptionEntry.py +85 -19
- edsl/jobs/runners/JobsRunnerAsyncio.py +146 -175
- edsl/jobs/runners/JobsRunnerStatus.py +331 -0
- edsl/jobs/tasks/QuestionTaskCreator.py +30 -23
- edsl/jobs/tasks/TaskHistory.py +148 -213
- edsl/language_models/LanguageModel.py +261 -156
- edsl/language_models/ModelList.py +2 -2
- edsl/language_models/RegisterLanguageModelsMeta.py +14 -29
- edsl/language_models/fake_openai_call.py +15 -0
- edsl/language_models/fake_openai_service.py +61 -0
- edsl/language_models/registry.py +23 -6
- edsl/language_models/repair.py +0 -19
- edsl/language_models/utilities.py +61 -0
- edsl/notebooks/Notebook.py +20 -2
- edsl/prompts/Prompt.py +52 -2
- edsl/questions/AnswerValidatorMixin.py +23 -26
- edsl/questions/QuestionBase.py +330 -249
- edsl/questions/QuestionBaseGenMixin.py +133 -0
- edsl/questions/QuestionBasePromptsMixin.py +266 -0
- edsl/questions/QuestionBudget.py +99 -41
- edsl/questions/QuestionCheckBox.py +227 -35
- edsl/questions/QuestionExtract.py +98 -27
- edsl/questions/QuestionFreeText.py +52 -29
- edsl/questions/QuestionFunctional.py +7 -0
- edsl/questions/QuestionList.py +141 -22
- edsl/questions/QuestionMultipleChoice.py +159 -65
- edsl/questions/QuestionNumerical.py +88 -46
- edsl/questions/QuestionRank.py +182 -24
- edsl/questions/Quick.py +41 -0
- edsl/questions/RegisterQuestionsMeta.py +31 -12
- edsl/questions/ResponseValidatorABC.py +170 -0
- edsl/questions/__init__.py +3 -4
- edsl/questions/decorators.py +21 -0
- edsl/questions/derived/QuestionLikertFive.py +10 -5
- edsl/questions/derived/QuestionLinearScale.py +15 -2
- edsl/questions/derived/QuestionTopK.py +10 -1
- edsl/questions/derived/QuestionYesNo.py +24 -3
- edsl/questions/descriptors.py +43 -7
- edsl/questions/prompt_templates/question_budget.jinja +13 -0
- edsl/questions/prompt_templates/question_checkbox.jinja +32 -0
- edsl/questions/prompt_templates/question_extract.jinja +11 -0
- edsl/questions/prompt_templates/question_free_text.jinja +3 -0
- edsl/questions/prompt_templates/question_linear_scale.jinja +11 -0
- edsl/questions/prompt_templates/question_list.jinja +17 -0
- edsl/questions/prompt_templates/question_multiple_choice.jinja +33 -0
- edsl/questions/prompt_templates/question_numerical.jinja +37 -0
- edsl/questions/question_registry.py +6 -2
- edsl/questions/templates/__init__.py +0 -0
- edsl/questions/templates/budget/__init__.py +0 -0
- edsl/questions/templates/budget/answering_instructions.jinja +7 -0
- edsl/questions/templates/budget/question_presentation.jinja +7 -0
- edsl/questions/templates/checkbox/__init__.py +0 -0
- edsl/questions/templates/checkbox/answering_instructions.jinja +10 -0
- edsl/questions/templates/checkbox/question_presentation.jinja +22 -0
- edsl/questions/templates/extract/__init__.py +0 -0
- edsl/questions/templates/extract/answering_instructions.jinja +7 -0
- edsl/questions/templates/extract/question_presentation.jinja +1 -0
- edsl/questions/templates/free_text/__init__.py +0 -0
- edsl/questions/templates/free_text/answering_instructions.jinja +0 -0
- edsl/questions/templates/free_text/question_presentation.jinja +1 -0
- edsl/questions/templates/likert_five/__init__.py +0 -0
- edsl/questions/templates/likert_five/answering_instructions.jinja +10 -0
- edsl/questions/templates/likert_five/question_presentation.jinja +12 -0
- edsl/questions/templates/linear_scale/__init__.py +0 -0
- edsl/questions/templates/linear_scale/answering_instructions.jinja +5 -0
- edsl/questions/templates/linear_scale/question_presentation.jinja +5 -0
- edsl/questions/templates/list/__init__.py +0 -0
- edsl/questions/templates/list/answering_instructions.jinja +4 -0
- edsl/questions/templates/list/question_presentation.jinja +5 -0
- edsl/questions/templates/multiple_choice/__init__.py +0 -0
- edsl/questions/templates/multiple_choice/answering_instructions.jinja +9 -0
- edsl/questions/templates/multiple_choice/html.jinja +0 -0
- edsl/questions/templates/multiple_choice/question_presentation.jinja +12 -0
- edsl/questions/templates/numerical/__init__.py +0 -0
- edsl/questions/templates/numerical/answering_instructions.jinja +8 -0
- edsl/questions/templates/numerical/question_presentation.jinja +7 -0
- edsl/questions/templates/rank/__init__.py +0 -0
- edsl/questions/templates/rank/answering_instructions.jinja +11 -0
- edsl/questions/templates/rank/question_presentation.jinja +15 -0
- edsl/questions/templates/top_k/__init__.py +0 -0
- edsl/questions/templates/top_k/answering_instructions.jinja +8 -0
- edsl/questions/templates/top_k/question_presentation.jinja +22 -0
- edsl/questions/templates/yes_no/__init__.py +0 -0
- edsl/questions/templates/yes_no/answering_instructions.jinja +6 -0
- edsl/questions/templates/yes_no/question_presentation.jinja +12 -0
- edsl/results/Dataset.py +20 -0
- edsl/results/DatasetExportMixin.py +46 -48
- edsl/results/DatasetTree.py +145 -0
- edsl/results/Result.py +32 -5
- edsl/results/Results.py +135 -46
- edsl/results/ResultsDBMixin.py +3 -3
- edsl/results/Selector.py +118 -0
- edsl/results/tree_explore.py +115 -0
- edsl/scenarios/FileStore.py +71 -10
- edsl/scenarios/Scenario.py +96 -25
- edsl/scenarios/ScenarioImageMixin.py +2 -2
- edsl/scenarios/ScenarioList.py +361 -39
- edsl/scenarios/ScenarioListExportMixin.py +9 -0
- edsl/scenarios/ScenarioListPdfMixin.py +150 -4
- edsl/study/SnapShot.py +8 -1
- edsl/study/Study.py +32 -0
- edsl/surveys/Rule.py +10 -1
- edsl/surveys/RuleCollection.py +21 -5
- edsl/surveys/Survey.py +637 -311
- edsl/surveys/SurveyExportMixin.py +71 -9
- edsl/surveys/SurveyFlowVisualizationMixin.py +2 -1
- edsl/surveys/SurveyQualtricsImport.py +75 -4
- edsl/surveys/instructions/ChangeInstruction.py +47 -0
- edsl/surveys/instructions/Instruction.py +34 -0
- edsl/surveys/instructions/InstructionCollection.py +77 -0
- edsl/surveys/instructions/__init__.py +0 -0
- edsl/templates/error_reporting/base.html +24 -0
- edsl/templates/error_reporting/exceptions_by_model.html +35 -0
- edsl/templates/error_reporting/exceptions_by_question_name.html +17 -0
- edsl/templates/error_reporting/exceptions_by_type.html +17 -0
- edsl/templates/error_reporting/interview_details.html +116 -0
- edsl/templates/error_reporting/interviews.html +10 -0
- edsl/templates/error_reporting/overview.html +5 -0
- edsl/templates/error_reporting/performance_plot.html +2 -0
- edsl/templates/error_reporting/report.css +74 -0
- edsl/templates/error_reporting/report.html +118 -0
- edsl/templates/error_reporting/report.js +25 -0
- edsl/utilities/utilities.py +9 -1
- {edsl-0.1.32.dist-info → edsl-0.1.33.dist-info}/METADATA +5 -2
- edsl-0.1.33.dist-info/RECORD +295 -0
- edsl/jobs/interviews/InterviewTaskBuildingMixin.py +0 -286
- edsl/jobs/interviews/retry_management.py +0 -37
- edsl/jobs/runners/JobsRunnerStatusMixin.py +0 -333
- edsl/utilities/gcp_bucket/simple_example.py +0 -9
- edsl-0.1.32.dist-info/RECORD +0 -209
- {edsl-0.1.32.dist-info → edsl-0.1.33.dist-info}/LICENSE +0 -0
- {edsl-0.1.32.dist-info → edsl-0.1.33.dist-info}/WHEEL +0 -0
edsl/questions/QuestionBase.py
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
"""This module contains the Question class, which is the base class for all questions in EDSL."""
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
|
-
import time
|
5
4
|
from abc import ABC, abstractmethod
|
6
|
-
from typing import Any, Type, Optional, List, Callable
|
5
|
+
from typing import Any, Type, Optional, List, Callable, Union, TypedDict
|
7
6
|
import copy
|
8
7
|
|
9
8
|
from edsl.exceptions import (
|
10
9
|
QuestionResponseValidationError,
|
10
|
+
QuestionAnswerValidationError,
|
11
11
|
QuestionSerializationError,
|
12
12
|
)
|
13
13
|
from edsl.questions.descriptors import QuestionNameDescriptor, QuestionTextDescriptor
|
@@ -19,6 +19,8 @@ from edsl.Base import PersistenceMixin, RichPrintingMixin
|
|
19
19
|
from edsl.BaseDiff import BaseDiff, BaseDiffCollection
|
20
20
|
|
21
21
|
from edsl.questions.SimpleAskMixin import SimpleAskMixin
|
22
|
+
from edsl.questions.QuestionBasePromptsMixin import QuestionBasePromptsMixin
|
23
|
+
from edsl.questions.QuestionBaseGenMixin import QuestionBaseGenMixin
|
22
24
|
from edsl.utilities.decorators import add_edsl_version, remove_edsl_version
|
23
25
|
|
24
26
|
|
@@ -26,80 +28,128 @@ class QuestionBase(
|
|
26
28
|
PersistenceMixin,
|
27
29
|
RichPrintingMixin,
|
28
30
|
SimpleAskMixin,
|
31
|
+
QuestionBasePromptsMixin,
|
32
|
+
QuestionBaseGenMixin,
|
29
33
|
ABC,
|
30
34
|
AnswerValidatorMixin,
|
31
35
|
metaclass=RegisterQuestionsMeta,
|
32
36
|
):
|
33
|
-
"""ABC for the Question class. All questions
|
37
|
+
"""ABC for the Question class. All questions inherit from this class.
|
38
|
+
Some of the constraints on child questions are defined in the RegisterQuestionsMeta metaclass.
|
39
|
+
"""
|
34
40
|
|
35
41
|
question_name: str = QuestionNameDescriptor()
|
36
42
|
question_text: str = QuestionTextDescriptor()
|
37
43
|
|
38
|
-
|
39
|
-
|
40
|
-
return getattr(self, key)
|
44
|
+
_answering_instructions = None
|
45
|
+
_question_presentation = None
|
41
46
|
|
42
|
-
|
43
|
-
|
44
|
-
|
47
|
+
# region: Validation and simulation methods
|
48
|
+
@property
|
49
|
+
def response_validator(self) -> "ResponseValidatorBase":
|
50
|
+
"""Return the response validator."""
|
51
|
+
params = (
|
52
|
+
{
|
53
|
+
"response_model": self.response_model,
|
54
|
+
}
|
55
|
+
| {k: getattr(self, k) for k in self.validator_parameters}
|
56
|
+
| {"exception_to_throw": getattr(self, "exception_to_throw", None)}
|
57
|
+
| {"override_answer": getattr(self, "override_answer", None)}
|
58
|
+
)
|
59
|
+
return self.response_validator_class(**params)
|
45
60
|
|
46
|
-
|
61
|
+
@property
|
62
|
+
def validator_parameters(self) -> list[str]:
|
63
|
+
"""Return the parameters required for the response validator.
|
47
64
|
|
48
|
-
|
49
|
-
|
65
|
+
>>> from edsl import QuestionMultipleChoice as Q
|
66
|
+
>>> Q.example().validator_parameters
|
67
|
+
['question_options', 'use_code']
|
50
68
|
|
51
|
-
|
52
|
-
|
53
|
-
_ = data.pop("edsl_version")
|
54
|
-
_ = data.pop("edsl_class_name")
|
55
|
-
except KeyError:
|
56
|
-
print("Serialized question lacks edsl version, but is should have it.")
|
69
|
+
"""
|
70
|
+
return self.response_validator_class.required_params
|
57
71
|
|
58
|
-
|
72
|
+
@property
|
73
|
+
def fake_data_factory(self):
|
74
|
+
"""Return the fake data factory."""
|
75
|
+
if not hasattr(self, "_fake_data_factory"):
|
76
|
+
from polyfactory.factories.pydantic_factory import ModelFactory
|
77
|
+
|
78
|
+
class FakeData(ModelFactory[self.response_model]):
|
79
|
+
...
|
80
|
+
|
81
|
+
self._fake_data_factory = FakeData
|
82
|
+
return self._fake_data_factory
|
83
|
+
|
84
|
+
def _simulate_answer(self, human_readable: bool = False) -> dict:
|
85
|
+
"""Simulate a valid answer for debugging purposes (what the validator expects).
|
86
|
+
>>> from edsl import QuestionFreeText as Q
|
87
|
+
>>> Q.example()._simulate_answer()
|
88
|
+
{'answer': '...', 'generated_tokens': ...}
|
89
|
+
"""
|
90
|
+
simulated_answer = self.fake_data_factory.build().dict()
|
91
|
+
if human_readable and hasattr(self, "question_options") and self.use_code:
|
92
|
+
simulated_answer["answer"] = [
|
93
|
+
self.question_options[index] for index in simulated_answer["answer"]
|
94
|
+
]
|
95
|
+
return simulated_answer
|
96
|
+
|
97
|
+
class ValidatedAnswer(TypedDict):
|
98
|
+
answer: Any
|
99
|
+
comment: Optional[str]
|
100
|
+
generated_tokens: Optional[str]
|
101
|
+
|
102
|
+
def _validate_answer(self, answer: dict) -> ValidatedAnswer:
|
103
|
+
"""Validate the answer.
|
104
|
+
>>> from edsl.exceptions import QuestionAnswerValidationError
|
105
|
+
>>> from edsl import QuestionFreeText as Q
|
106
|
+
>>> Q.example()._validate_answer({'answer': 'Hello', 'generated_tokens': 'Hello'})
|
107
|
+
{'answer': 'Hello', 'generated_tokens': 'Hello'}
|
108
|
+
"""
|
59
109
|
|
60
|
-
|
61
|
-
"""Apply a function to the question parts
|
110
|
+
return self.response_validator.validate(answer)
|
62
111
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
112
|
+
# endregion
|
113
|
+
|
114
|
+
# region: Serialization methods
|
115
|
+
@property
|
116
|
+
def name(self) -> str:
|
117
|
+
"Helper function so questions and instructions can use the same access method"
|
118
|
+
return self.question_name
|
68
119
|
|
120
|
+
def __hash__(self) -> int:
|
121
|
+
"""Return a hash of the question.
|
122
|
+
|
123
|
+
>>> from edsl import QuestionFreeText as Q
|
124
|
+
>>> hash(Q.example())
|
125
|
+
1144312636257752766
|
69
126
|
"""
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
d = copy.deepcopy(self._to_dict())
|
74
|
-
for key, value in d.items():
|
75
|
-
if key in exclude_components:
|
76
|
-
continue
|
77
|
-
if isinstance(value, dict):
|
78
|
-
for k, v in value.items():
|
79
|
-
value[k] = func(v)
|
80
|
-
d[key] = value
|
81
|
-
continue
|
82
|
-
if isinstance(value, list):
|
83
|
-
value = [func(v) for v in value]
|
84
|
-
d[key] = value
|
85
|
-
continue
|
86
|
-
d[key] = func(value)
|
87
|
-
return QuestionBase.from_dict(d)
|
127
|
+
from edsl.utilities.utilities import dict_hash
|
128
|
+
|
129
|
+
return dict_hash(self._to_dict())
|
88
130
|
|
89
131
|
@property
|
90
132
|
def data(self) -> dict:
|
91
|
-
"""Return a dictionary of question attributes **except** for question_type.
|
133
|
+
"""Return a dictionary of question attributes **except** for question_type.
|
134
|
+
|
135
|
+
>>> from edsl import QuestionFreeText as Q
|
136
|
+
>>> Q.example().data
|
137
|
+
{'question_name': 'how_are_you', 'question_text': 'How are you?'}
|
138
|
+
"""
|
139
|
+
exclude_list = [
|
140
|
+
"question_type",
|
141
|
+
"_include_comment",
|
142
|
+
"_fake_data_factory",
|
143
|
+
"_use_code",
|
144
|
+
"_answering_instructions",
|
145
|
+
"_question_presentation",
|
146
|
+
"_model_instructions",
|
147
|
+
]
|
92
148
|
candidate_data = {
|
93
149
|
k.replace("_", "", 1): v
|
94
150
|
for k, v in self.__dict__.items()
|
95
|
-
if k.startswith("_")
|
151
|
+
if k.startswith("_") and k not in exclude_list
|
96
152
|
}
|
97
|
-
optional_attributes = {
|
98
|
-
"set_instructions": "instructions",
|
99
|
-
}
|
100
|
-
for boolean_flag, attribute in optional_attributes.items():
|
101
|
-
if hasattr(self, boolean_flag) and not getattr(self, boolean_flag):
|
102
|
-
candidate_data.pop(attribute, None)
|
103
153
|
|
104
154
|
if "func" in candidate_data:
|
105
155
|
func = candidate_data.pop("func")
|
@@ -109,147 +159,22 @@ class QuestionBase(
|
|
109
159
|
|
110
160
|
return candidate_data
|
111
161
|
|
112
|
-
|
113
|
-
|
114
|
-
cls, model: Optional[str] = None
|
115
|
-
) -> list[type["PromptBase"]]:
|
116
|
-
"""Get the prompts that are applicable to the question type.
|
117
|
-
|
118
|
-
:param model: The language model to use.
|
119
|
-
|
120
|
-
>>> from edsl.questions import QuestionFreeText
|
121
|
-
>>> QuestionFreeText.applicable_prompts()
|
122
|
-
[<class 'edsl.prompts.library.question_freetext.FreeText'>]
|
123
|
-
|
124
|
-
:param model: The language model to use. If None, assumes does not matter.
|
125
|
-
|
126
|
-
"""
|
127
|
-
from edsl.prompts.registry import get_classes as prompt_lookup
|
128
|
-
|
129
|
-
applicable_prompts = prompt_lookup(
|
130
|
-
component_type="question_instructions",
|
131
|
-
question_type=cls.question_type,
|
132
|
-
model=model,
|
133
|
-
)
|
134
|
-
return applicable_prompts
|
135
|
-
|
136
|
-
@property
|
137
|
-
def model_instructions(self) -> dict:
|
138
|
-
"""Get the model-specific instructions for the question."""
|
139
|
-
if not hasattr(self, "_model_instructions"):
|
140
|
-
self._model_instructions = {}
|
141
|
-
return self._model_instructions
|
142
|
-
|
143
|
-
def _all_text(self) -> str:
|
144
|
-
"""Return the question text."""
|
145
|
-
txt = ""
|
146
|
-
for key, value in self.data.items():
|
147
|
-
if isinstance(value, str):
|
148
|
-
txt += value
|
149
|
-
elif isinstance(value, list):
|
150
|
-
txt += "".join(str(value))
|
151
|
-
return txt
|
152
|
-
|
153
|
-
@property
|
154
|
-
def parameters(self) -> set[str]:
|
155
|
-
"""Return the parameters of the question."""
|
156
|
-
from jinja2 import Environment, meta
|
157
|
-
|
158
|
-
env = Environment()
|
159
|
-
# Parse the template
|
160
|
-
txt = self._all_text()
|
161
|
-
# txt = self.question_text
|
162
|
-
# if hasattr(self, "question_options"):
|
163
|
-
# txt += " ".join(self.question_options)
|
164
|
-
parsed_content = env.parse(txt)
|
165
|
-
# Extract undeclared variables
|
166
|
-
variables = meta.find_undeclared_variables(parsed_content)
|
167
|
-
# Return as a list
|
168
|
-
return set(variables)
|
169
|
-
|
170
|
-
@model_instructions.setter
|
171
|
-
def model_instructions(self, data: dict):
|
172
|
-
"""Set the model-specific instructions for the question."""
|
173
|
-
self._model_instructions = data
|
174
|
-
|
175
|
-
def add_model_instructions(
|
176
|
-
self, *, instructions: str, model: Optional[str] = None
|
177
|
-
) -> None:
|
178
|
-
"""Add model-specific instructions for the question that override the default instructions.
|
179
|
-
|
180
|
-
:param instructions: The instructions to add. This is typically a jinja2 template.
|
181
|
-
:param model: The language model for this instruction.
|
182
|
-
|
183
|
-
>>> from edsl.questions import QuestionFreeText
|
184
|
-
>>> q = QuestionFreeText(question_name = "color", question_text = "What is your favorite color?")
|
185
|
-
>>> q.add_model_instructions(instructions = "{{question_text}}. Answer in valid JSON like so {'answer': 'comment: <>}", model = "gpt3")
|
186
|
-
>>> q.get_instructions(model = "gpt3")
|
187
|
-
Prompt(text=\"""{{question_text}}. Answer in valid JSON like so {'answer': 'comment: <>}\""")
|
188
|
-
"""
|
189
|
-
from edsl import Model
|
190
|
-
|
191
|
-
if not hasattr(self, "_model_instructions"):
|
192
|
-
self._model_instructions = {}
|
193
|
-
if model is None:
|
194
|
-
# if not model is passed, all the models are mapped to this instruction, including 'None'
|
195
|
-
self._model_instructions = {
|
196
|
-
model_name: instructions
|
197
|
-
for model_name in Model.available(name_only=True)
|
198
|
-
}
|
199
|
-
self._model_instructions.update({model: instructions})
|
200
|
-
else:
|
201
|
-
self._model_instructions.update({model: instructions})
|
202
|
-
|
203
|
-
def get_instructions(self, model: Optional[str] = None) -> type["PromptBase"]:
|
204
|
-
"""Get the mathcing question-answering instructions for the question.
|
205
|
-
|
206
|
-
:param model: The language model to use.
|
162
|
+
def _to_dict(self):
|
163
|
+
"""Convert the question to a dictionary that includes the question type (used in deserialization).
|
207
164
|
|
208
|
-
>>> from edsl import QuestionFreeText
|
209
|
-
|
210
|
-
Prompt(text=\"""You are being asked the following question: {{question_text}}
|
211
|
-
Return a valid JSON formatted like this:
|
212
|
-
{"answer": "<put free text answer here>"}
|
213
|
-
\""")
|
165
|
+
>>> from edsl import QuestionFreeText as Q; Q.example()._to_dict()
|
166
|
+
{'question_name': 'how_are_you', 'question_text': 'How are you?', 'question_type': 'free_text'}
|
214
167
|
"""
|
215
|
-
from edsl.prompts.Prompt import Prompt
|
216
|
-
|
217
|
-
if model in self.model_instructions:
|
218
|
-
return Prompt(text=self.model_instructions[model])
|
219
|
-
else:
|
220
|
-
return self.applicable_prompts(model)[0]()
|
221
|
-
|
222
|
-
def option_permutations(self) -> list[QuestionBase]:
|
223
|
-
"""Return a list of questions with all possible permutations of the options."""
|
224
|
-
|
225
|
-
if not hasattr(self, "question_options"):
|
226
|
-
return [self]
|
227
|
-
|
228
|
-
import copy
|
229
|
-
import itertools
|
230
|
-
|
231
|
-
questions = []
|
232
|
-
for index, permutation in enumerate(
|
233
|
-
itertools.permutations(self.question_options)
|
234
|
-
):
|
235
|
-
question = copy.deepcopy(self)
|
236
|
-
question.question_options = list(permutation)
|
237
|
-
question.question_name = f"{self.question_name}_{index}"
|
238
|
-
questions.append(question)
|
239
|
-
return questions
|
240
|
-
|
241
|
-
############################
|
242
|
-
# Serialization methods
|
243
|
-
############################
|
244
|
-
def _to_dict(self):
|
245
|
-
"""Convert the question to a dictionary that includes the question type (used in deserialization)."""
|
246
168
|
candidate_data = self.data.copy()
|
247
169
|
candidate_data["question_type"] = self.question_type
|
248
170
|
return candidate_data
|
249
171
|
|
250
172
|
@add_edsl_version
|
251
173
|
def to_dict(self) -> dict[str, Any]:
|
252
|
-
"""Convert the question to a dictionary that includes the question type (used in deserialization).
|
174
|
+
"""Convert the question to a dictionary that includes the question type (used in deserialization).
|
175
|
+
>>> from edsl import QuestionFreeText as Q; Q.example().to_dict()
|
176
|
+
{'question_name': 'how_are_you', 'question_text': 'How are you?', 'question_type': 'free_text', 'edsl_version': '...'}
|
177
|
+
"""
|
253
178
|
return self._to_dict()
|
254
179
|
|
255
180
|
@classmethod
|
@@ -289,39 +214,90 @@ class QuestionBase(
|
|
289
214
|
|
290
215
|
return question_class(**local_data)
|
291
216
|
|
292
|
-
|
293
|
-
"""Return a deep copy of the question."""
|
294
|
-
return copy.deepcopy(self)
|
217
|
+
# endregion
|
295
218
|
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
from
|
301
|
-
import json
|
219
|
+
# region: Running methods
|
220
|
+
@classmethod
|
221
|
+
def _get_test_model(self, canned_response: Optional[str] = None) -> "LanguageModel":
|
222
|
+
"""Get a test model for the question."""
|
223
|
+
from edsl.language_models import LanguageModel
|
302
224
|
|
303
|
-
|
225
|
+
return LanguageModel.example(canned_response=canned_response, test_model=True)
|
226
|
+
|
227
|
+
@classmethod
|
228
|
+
def run_example(
|
229
|
+
cls,
|
230
|
+
show_answer: bool = True,
|
231
|
+
model: Optional["LanguageModel"] = None,
|
232
|
+
cache=False,
|
233
|
+
**kwargs,
|
234
|
+
):
|
235
|
+
"""Run an example of the question.
|
236
|
+
>>> from edsl.language_models import LanguageModel
|
237
|
+
>>> from edsl import QuestionFreeText as Q
|
238
|
+
>>> m = Q._get_test_model(canned_response = "Yo, what's up?")
|
239
|
+
>>> m.execute_model_call("", "")
|
240
|
+
{'message': [{'text': "Yo, what's up?"}], 'usage': {'prompt_tokens': 1, 'completion_tokens': 1}}
|
241
|
+
>>> Q.run_example(show_answer = True, model = m)
|
242
|
+
┏━━━━━━━━━━━━━━━━┓
|
243
|
+
┃ answer ┃
|
244
|
+
┃ .how_are_you ┃
|
245
|
+
┡━━━━━━━━━━━━━━━━┩
|
246
|
+
│ Yo, what's up? │
|
247
|
+
└────────────────┘
|
248
|
+
"""
|
249
|
+
if model is None:
|
250
|
+
from edsl import Model
|
251
|
+
|
252
|
+
model = Model()
|
253
|
+
results = cls.example(**kwargs).by(model).run(cache=cache)
|
254
|
+
if show_answer:
|
255
|
+
results.select("answer.*").print()
|
256
|
+
else:
|
257
|
+
return results
|
304
258
|
|
305
259
|
def __call__(self, just_answer=True, model=None, agent=None, **kwargs):
|
306
260
|
"""Call the question.
|
307
261
|
|
308
|
-
|
309
|
-
>>>
|
310
|
-
>>>
|
311
|
-
>>> q =
|
262
|
+
|
263
|
+
>>> from edsl import QuestionFreeText as Q
|
264
|
+
>>> m = Q._get_test_model(canned_response = "Yo, what's up?")
|
265
|
+
>>> q = Q(question_name = "color", question_text = "What is your favorite color?")
|
312
266
|
>>> q(model = m)
|
313
267
|
"Yo, what's up?"
|
314
268
|
|
315
269
|
"""
|
316
270
|
survey = self.to_survey()
|
317
|
-
results = survey(model=model, agent=agent, **kwargs)
|
271
|
+
results = survey(model=model, agent=agent, **kwargs, cache=False)
|
318
272
|
if just_answer:
|
319
273
|
return results.select(f"answer.{self.question_name}").first()
|
320
274
|
else:
|
321
275
|
return results
|
322
276
|
|
323
|
-
|
324
|
-
"""
|
277
|
+
def run(self, *args, **kwargs) -> "Results":
|
278
|
+
"""Turn a single question into a survey and runs it."""
|
279
|
+
from edsl.surveys.Survey import Survey
|
280
|
+
|
281
|
+
s = self.to_survey()
|
282
|
+
return s.run(*args, **kwargs)
|
283
|
+
|
284
|
+
async def run_async(
|
285
|
+
self,
|
286
|
+
just_answer: bool = True,
|
287
|
+
model: Optional["Model"] = None,
|
288
|
+
agent: Optional["Agent"] = None,
|
289
|
+
**kwargs,
|
290
|
+
) -> Union[Any, "Results"]:
|
291
|
+
"""Call the question asynchronously.
|
292
|
+
|
293
|
+
>>> import asyncio
|
294
|
+
>>> from edsl import QuestionFreeText as Q
|
295
|
+
>>> m = Q._get_test_model(canned_response = "Blue")
|
296
|
+
>>> q = Q(question_name = "color", question_text = "What is your favorite color?")
|
297
|
+
>>> async def test_run_async(): result = await q.run_async(model=m); print(result)
|
298
|
+
>>> asyncio.run(test_run_async())
|
299
|
+
Blue
|
300
|
+
"""
|
325
301
|
survey = self.to_survey()
|
326
302
|
results = await survey.run_async(model=model, agent=agent, **kwargs)
|
327
303
|
if just_answer:
|
@@ -329,9 +305,37 @@ class QuestionBase(
|
|
329
305
|
else:
|
330
306
|
return results
|
331
307
|
|
308
|
+
# endregion
|
309
|
+
|
310
|
+
# region: Magic methods
|
311
|
+
def _repr_html_(self):
|
312
|
+
from edsl.utilities.utilities import data_to_html
|
313
|
+
|
314
|
+
data = self.to_dict()
|
315
|
+
try:
|
316
|
+
_ = data.pop("edsl_version")
|
317
|
+
_ = data.pop("edsl_class_name")
|
318
|
+
except KeyError:
|
319
|
+
print("Serialized question lacks edsl version, but is should have it.")
|
320
|
+
|
321
|
+
return data_to_html(data)
|
322
|
+
|
323
|
+
def __getitem__(self, key: str) -> Any:
|
324
|
+
"""Get an attribute of the question so it can be treated like a dictionary.
|
325
|
+
|
326
|
+
>>> from edsl.questions import QuestionFreeText as Q
|
327
|
+
>>> Q.example()['question_text']
|
328
|
+
'How are you?'
|
329
|
+
"""
|
330
|
+
return getattr(self, key)
|
331
|
+
|
332
332
|
def __repr__(self) -> str:
|
333
|
-
"""Return a string representation of the question. Should be able to be used to reconstruct the question.
|
334
|
-
|
333
|
+
"""Return a string representation of the question. Should be able to be used to reconstruct the question.
|
334
|
+
|
335
|
+
>>> from edsl import QuestionFreeText as Q
|
336
|
+
>>> repr(Q.example())
|
337
|
+
'Question(\\'free_text\\', question_name = \"""how_are_you\""", question_text = \"""How are you?\""")'
|
338
|
+
"""
|
335
339
|
items = [
|
336
340
|
f'{k} = """{v}"""' if isinstance(v, str) else f"{k} = {v}"
|
337
341
|
for k, v in self.data.items()
|
@@ -340,14 +344,31 @@ class QuestionBase(
|
|
340
344
|
question_type = self.to_dict().get("question_type", "None")
|
341
345
|
return f"Question('{question_type}', {', '.join(items)})"
|
342
346
|
|
343
|
-
def __eq__(self, other: Type[QuestionBase]) -> bool:
|
344
|
-
"""Check if two questions are equal. Equality is defined as having the .to_dict().
|
347
|
+
def __eq__(self, other: Union[Any, Type[QuestionBase]]) -> bool:
|
348
|
+
"""Check if two questions are equal. Equality is defined as having the .to_dict().
|
349
|
+
|
350
|
+
>>> from edsl import QuestionFreeText as Q
|
351
|
+
>>> q1 = Q.example()
|
352
|
+
>>> q2 = Q.example()
|
353
|
+
>>> q1 == q2
|
354
|
+
True
|
355
|
+
>>> q1.question_text = "How are you John?"
|
356
|
+
>>> q1 == q2
|
357
|
+
False
|
358
|
+
|
359
|
+
"""
|
345
360
|
if not isinstance(other, QuestionBase):
|
346
361
|
return False
|
347
362
|
return self.to_dict() == other.to_dict()
|
348
363
|
|
349
364
|
def __sub__(self, other) -> BaseDiff:
|
350
|
-
"""Return the difference between two objects.
|
365
|
+
"""Return the difference between two objects.
|
366
|
+
>>> from edsl import QuestionFreeText as Q
|
367
|
+
>>> q1 = Q.example()
|
368
|
+
>>> q2 = q1.copy()
|
369
|
+
>>> q2.question_text = "How are you John?"
|
370
|
+
>>> diff = q1 - q2
|
371
|
+
"""
|
351
372
|
|
352
373
|
return BaseDiff(other, self)
|
353
374
|
|
@@ -364,57 +385,51 @@ class QuestionBase(
|
|
364
385
|
):
|
365
386
|
return other_question_or_diff.apply(self)
|
366
387
|
|
367
|
-
from edsl.questions import compose_questions
|
368
|
-
|
369
|
-
return compose_questions(self, other_question_or_diff)
|
388
|
+
# from edsl.questions import compose_questions
|
389
|
+
# return compose_questions(self, other_question_or_diff)
|
370
390
|
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
if "answer" not in response:
|
379
|
-
raise QuestionResponseValidationError(
|
380
|
-
"Response from LLM does not have an answer"
|
381
|
-
)
|
382
|
-
return response
|
391
|
+
# def _validate_response(self, response):
|
392
|
+
# """Validate the response from the LLM. Behavior depends on the question type."""
|
393
|
+
# if "answer" not in response:
|
394
|
+
# raise QuestionResponseValidationError(
|
395
|
+
# "Response from LLM does not have an answer"
|
396
|
+
# )
|
397
|
+
# return response
|
383
398
|
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
399
|
+
def _translate_answer_code_to_answer(
|
400
|
+
self, answer, scenario: Optional["Scenario"] = None
|
401
|
+
):
|
402
|
+
"""There is over-ridden by child classes that ask for codes."""
|
403
|
+
return answer
|
388
404
|
|
389
|
-
|
390
|
-
def _simulate_answer(self, human_readable=True) -> dict: # pragma: no cover
|
391
|
-
"""Simulate a valid answer for debugging purposes (what the validator expects)."""
|
392
|
-
pass
|
405
|
+
# endregion
|
393
406
|
|
394
|
-
|
395
|
-
# Forward methods
|
396
|
-
############################
|
407
|
+
# region: Forward methods
|
397
408
|
def add_question(self, other: QuestionBase) -> "Survey":
|
398
|
-
"""Add a question to this question by turning them into a survey with two questions.
|
409
|
+
"""Add a question to this question by turning them into a survey with two questions.
|
410
|
+
|
411
|
+
>>> from edsl.questions import QuestionFreeText as Q
|
412
|
+
>>> from edsl.questions import QuestionMultipleChoice as QMC
|
413
|
+
>>> s = Q.example().add_question(QMC.example())
|
414
|
+
>>> len(s.questions)
|
415
|
+
2
|
416
|
+
"""
|
399
417
|
from edsl.surveys.Survey import Survey
|
400
418
|
|
401
419
|
s = Survey([self, other])
|
402
420
|
return s
|
403
421
|
|
404
422
|
def to_survey(self) -> "Survey":
|
405
|
-
"""Turn a single question into a survey.
|
423
|
+
"""Turn a single question into a survey.
|
424
|
+
>>> from edsl import QuestionFreeText as Q
|
425
|
+
>>> Q.example().to_survey().questions[0].question_name
|
426
|
+
'how_are_you'
|
427
|
+
"""
|
406
428
|
from edsl.surveys.Survey import Survey
|
407
429
|
|
408
430
|
s = Survey([self])
|
409
431
|
return s
|
410
432
|
|
411
|
-
def run(self, *args, **kwargs) -> "Results":
|
412
|
-
"""Turn a single question into a survey and run it."""
|
413
|
-
from edsl.surveys.Survey import Survey
|
414
|
-
|
415
|
-
s = self.to_survey()
|
416
|
-
return s.run(*args, **kwargs)
|
417
|
-
|
418
433
|
def by(self, *args) -> "Jobs":
|
419
434
|
"""Turn a single question into a survey and then a Job."""
|
420
435
|
from edsl.surveys.Survey import Survey
|
@@ -422,6 +437,15 @@ class QuestionBase(
|
|
422
437
|
s = Survey([self])
|
423
438
|
return s.by(*args)
|
424
439
|
|
440
|
+
# endregion
|
441
|
+
|
442
|
+
# region: Display methods
|
443
|
+
def print(self):
|
444
|
+
from rich import print_json
|
445
|
+
import json
|
446
|
+
|
447
|
+
print_json(json.dumps(self.to_dict()))
|
448
|
+
|
425
449
|
def human_readable(self) -> str:
|
426
450
|
"""Print the question in a human readable format.
|
427
451
|
|
@@ -441,6 +465,8 @@ class QuestionBase(
|
|
441
465
|
def html(
|
442
466
|
self,
|
443
467
|
scenario: Optional[dict] = None,
|
468
|
+
agent: Optional[dict] = {},
|
469
|
+
answers: Optional[dict] = None,
|
444
470
|
include_question_name: bool = False,
|
445
471
|
height: Optional[int] = None,
|
446
472
|
width: Optional[int] = None,
|
@@ -452,6 +478,17 @@ class QuestionBase(
|
|
452
478
|
if scenario is None:
|
453
479
|
scenario = {}
|
454
480
|
|
481
|
+
prior_answers_dict = {}
|
482
|
+
|
483
|
+
if isinstance(answers, dict):
|
484
|
+
for key, value in answers.items():
|
485
|
+
if not key.endswith("_comment") and not key.endswith(
|
486
|
+
"_generated_tokens"
|
487
|
+
):
|
488
|
+
prior_answers_dict[key] = {"answer": value}
|
489
|
+
|
490
|
+
# breakpoint()
|
491
|
+
|
455
492
|
base_template = """
|
456
493
|
<div id="{{ question_name }}" class="survey_question" data-type="{{ question_type }}">
|
457
494
|
{% if include_question_name %}
|
@@ -471,13 +508,40 @@ class QuestionBase(
|
|
471
508
|
|
472
509
|
base_template = Template(base_template)
|
473
510
|
|
474
|
-
|
475
|
-
"
|
476
|
-
"
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
511
|
+
context = {
|
512
|
+
"scenario": scenario,
|
513
|
+
"agent": agent,
|
514
|
+
} | prior_answers_dict
|
515
|
+
|
516
|
+
# Render the question text
|
517
|
+
try:
|
518
|
+
question_text = Template(self.question_text).render(context)
|
519
|
+
except Exception as e:
|
520
|
+
print(
|
521
|
+
f"Error rendering question: question_text = {self.question_text}, error = {e}"
|
522
|
+
)
|
523
|
+
question_text = self.question_text
|
524
|
+
|
525
|
+
try:
|
526
|
+
question_content = Template(question_content).render(context)
|
527
|
+
except Exception as e:
|
528
|
+
print(
|
529
|
+
f"Error rendering question: question_content = {question_content}, error = {e}"
|
530
|
+
)
|
531
|
+
question_content = question_content
|
532
|
+
|
533
|
+
try:
|
534
|
+
params = {
|
535
|
+
"question_name": self.question_name,
|
536
|
+
"question_text": question_text,
|
537
|
+
"question_type": self.question_type,
|
538
|
+
"question_content": question_content,
|
539
|
+
"include_question_name": include_question_name,
|
540
|
+
}
|
541
|
+
except Exception as e:
|
542
|
+
raise ValueError(
|
543
|
+
f"Error rendering question: params = {params}, error = {e}"
|
544
|
+
)
|
481
545
|
rendered_html = base_template.render(**params)
|
482
546
|
|
483
547
|
if iframe:
|
@@ -496,6 +560,21 @@ class QuestionBase(
|
|
496
560
|
|
497
561
|
return rendered_html
|
498
562
|
|
563
|
+
@classmethod
|
564
|
+
def example_model(cls):
|
565
|
+
from edsl import Model
|
566
|
+
|
567
|
+
q = cls.example()
|
568
|
+
m = Model("test", canned_response=cls._simulate_answer(q)["answer"])
|
569
|
+
|
570
|
+
return m
|
571
|
+
|
572
|
+
@classmethod
|
573
|
+
def example_results(cls):
|
574
|
+
m = cls.example_model()
|
575
|
+
q = cls.example()
|
576
|
+
return q.by(m).run(cache=False)
|
577
|
+
|
499
578
|
def rich_print(self):
|
500
579
|
"""Print the question in a rich format."""
|
501
580
|
from rich.table import Table
|
@@ -519,6 +598,8 @@ class QuestionBase(
|
|
519
598
|
)
|
520
599
|
return table
|
521
600
|
|
601
|
+
# endregion
|
602
|
+
|
522
603
|
|
523
604
|
if __name__ == "__main__":
|
524
605
|
import doctest
|