edsl 0.1.33__py3-none-any.whl → 0.1.33.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 +3 -9
- edsl/__init__.py +3 -8
- edsl/__version__.py +1 -1
- edsl/agents/Agent.py +8 -40
- edsl/agents/AgentList.py +0 -43
- edsl/agents/Invigilator.py +219 -135
- edsl/agents/InvigilatorBase.py +59 -148
- edsl/agents/{PromptConstructor.py → PromptConstructionMixin.py} +89 -138
- edsl/agents/__init__.py +0 -1
- edsl/config.py +56 -47
- edsl/coop/coop.py +7 -50
- edsl/data/Cache.py +1 -35
- edsl/data_transfer_models.py +38 -73
- edsl/enums.py +0 -4
- edsl/exceptions/language_models.py +1 -25
- edsl/exceptions/questions.py +5 -62
- edsl/exceptions/results.py +0 -4
- edsl/inference_services/AnthropicService.py +11 -13
- edsl/inference_services/AwsBedrock.py +17 -19
- edsl/inference_services/AzureAI.py +20 -37
- edsl/inference_services/GoogleService.py +12 -16
- edsl/inference_services/GroqService.py +0 -2
- edsl/inference_services/InferenceServiceABC.py +3 -58
- edsl/inference_services/OpenAIService.py +54 -48
- edsl/inference_services/models_available_cache.py +6 -0
- edsl/inference_services/registry.py +0 -6
- edsl/jobs/Answers.py +12 -10
- edsl/jobs/Jobs.py +21 -36
- edsl/jobs/buckets/BucketCollection.py +15 -24
- edsl/jobs/buckets/TokenBucket.py +14 -93
- edsl/jobs/interviews/Interview.py +78 -366
- edsl/jobs/interviews/InterviewExceptionEntry.py +19 -85
- edsl/jobs/interviews/InterviewTaskBuildingMixin.py +286 -0
- edsl/jobs/interviews/{InterviewExceptionCollection.py → interview_exception_tracking.py} +68 -14
- edsl/jobs/interviews/retry_management.py +37 -0
- edsl/jobs/runners/JobsRunnerAsyncio.py +175 -146
- edsl/jobs/runners/JobsRunnerStatusMixin.py +333 -0
- edsl/jobs/tasks/QuestionTaskCreator.py +23 -30
- edsl/jobs/tasks/TaskHistory.py +213 -148
- edsl/language_models/LanguageModel.py +156 -261
- edsl/language_models/ModelList.py +2 -2
- edsl/language_models/RegisterLanguageModelsMeta.py +29 -14
- edsl/language_models/registry.py +6 -23
- edsl/language_models/repair.py +19 -0
- edsl/prompts/Prompt.py +2 -52
- edsl/questions/AnswerValidatorMixin.py +26 -23
- edsl/questions/QuestionBase.py +249 -329
- edsl/questions/QuestionBudget.py +41 -99
- edsl/questions/QuestionCheckBox.py +35 -227
- edsl/questions/QuestionExtract.py +27 -98
- edsl/questions/QuestionFreeText.py +29 -52
- edsl/questions/QuestionFunctional.py +0 -7
- edsl/questions/QuestionList.py +22 -141
- edsl/questions/QuestionMultipleChoice.py +65 -159
- edsl/questions/QuestionNumerical.py +46 -88
- edsl/questions/QuestionRank.py +24 -182
- edsl/questions/RegisterQuestionsMeta.py +12 -31
- edsl/questions/__init__.py +4 -3
- edsl/questions/derived/QuestionLikertFive.py +5 -10
- edsl/questions/derived/QuestionLinearScale.py +2 -15
- edsl/questions/derived/QuestionTopK.py +1 -10
- edsl/questions/derived/QuestionYesNo.py +3 -24
- edsl/questions/descriptors.py +7 -43
- edsl/questions/question_registry.py +2 -6
- edsl/results/Dataset.py +0 -20
- edsl/results/DatasetExportMixin.py +48 -46
- edsl/results/Result.py +5 -32
- edsl/results/Results.py +46 -135
- edsl/results/ResultsDBMixin.py +3 -3
- edsl/scenarios/FileStore.py +10 -71
- edsl/scenarios/Scenario.py +25 -96
- edsl/scenarios/ScenarioImageMixin.py +2 -2
- edsl/scenarios/ScenarioList.py +39 -361
- edsl/scenarios/ScenarioListExportMixin.py +0 -9
- edsl/scenarios/ScenarioListPdfMixin.py +4 -150
- edsl/study/SnapShot.py +1 -8
- edsl/study/Study.py +0 -32
- edsl/surveys/Rule.py +1 -10
- edsl/surveys/RuleCollection.py +5 -21
- edsl/surveys/Survey.py +310 -636
- edsl/surveys/SurveyExportMixin.py +9 -71
- edsl/surveys/SurveyFlowVisualizationMixin.py +1 -2
- edsl/surveys/SurveyQualtricsImport.py +4 -75
- edsl/utilities/gcp_bucket/simple_example.py +9 -0
- edsl/utilities/utilities.py +1 -9
- {edsl-0.1.33.dist-info → edsl-0.1.33.dev1.dist-info}/METADATA +2 -5
- edsl-0.1.33.dev1.dist-info/RECORD +209 -0
- edsl/TemplateLoader.py +0 -24
- edsl/auto/AutoStudy.py +0 -117
- edsl/auto/StageBase.py +0 -230
- edsl/auto/StageGenerateSurvey.py +0 -178
- edsl/auto/StageLabelQuestions.py +0 -125
- edsl/auto/StagePersona.py +0 -61
- edsl/auto/StagePersonaDimensionValueRanges.py +0 -88
- edsl/auto/StagePersonaDimensionValues.py +0 -74
- edsl/auto/StagePersonaDimensions.py +0 -69
- edsl/auto/StageQuestions.py +0 -73
- edsl/auto/SurveyCreatorPipeline.py +0 -21
- edsl/auto/utilities.py +0 -224
- edsl/coop/PriceFetcher.py +0 -58
- edsl/inference_services/MistralAIService.py +0 -120
- edsl/inference_services/TestService.py +0 -80
- edsl/inference_services/TogetherAIService.py +0 -170
- edsl/jobs/FailedQuestion.py +0 -78
- edsl/jobs/runners/JobsRunnerStatus.py +0 -331
- edsl/language_models/fake_openai_call.py +0 -15
- edsl/language_models/fake_openai_service.py +0 -61
- edsl/language_models/utilities.py +0 -61
- edsl/questions/QuestionBaseGenMixin.py +0 -133
- edsl/questions/QuestionBasePromptsMixin.py +0 -266
- edsl/questions/Quick.py +0 -41
- edsl/questions/ResponseValidatorABC.py +0 -170
- edsl/questions/decorators.py +0 -21
- edsl/questions/prompt_templates/question_budget.jinja +0 -13
- edsl/questions/prompt_templates/question_checkbox.jinja +0 -32
- edsl/questions/prompt_templates/question_extract.jinja +0 -11
- edsl/questions/prompt_templates/question_free_text.jinja +0 -3
- edsl/questions/prompt_templates/question_linear_scale.jinja +0 -11
- edsl/questions/prompt_templates/question_list.jinja +0 -17
- edsl/questions/prompt_templates/question_multiple_choice.jinja +0 -33
- edsl/questions/prompt_templates/question_numerical.jinja +0 -37
- edsl/questions/templates/__init__.py +0 -0
- edsl/questions/templates/budget/__init__.py +0 -0
- edsl/questions/templates/budget/answering_instructions.jinja +0 -7
- edsl/questions/templates/budget/question_presentation.jinja +0 -7
- edsl/questions/templates/checkbox/__init__.py +0 -0
- edsl/questions/templates/checkbox/answering_instructions.jinja +0 -10
- edsl/questions/templates/checkbox/question_presentation.jinja +0 -22
- edsl/questions/templates/extract/__init__.py +0 -0
- edsl/questions/templates/extract/answering_instructions.jinja +0 -7
- edsl/questions/templates/extract/question_presentation.jinja +0 -1
- 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 +0 -1
- edsl/questions/templates/likert_five/__init__.py +0 -0
- edsl/questions/templates/likert_five/answering_instructions.jinja +0 -10
- edsl/questions/templates/likert_five/question_presentation.jinja +0 -12
- edsl/questions/templates/linear_scale/__init__.py +0 -0
- edsl/questions/templates/linear_scale/answering_instructions.jinja +0 -5
- edsl/questions/templates/linear_scale/question_presentation.jinja +0 -5
- edsl/questions/templates/list/__init__.py +0 -0
- edsl/questions/templates/list/answering_instructions.jinja +0 -4
- edsl/questions/templates/list/question_presentation.jinja +0 -5
- edsl/questions/templates/multiple_choice/__init__.py +0 -0
- edsl/questions/templates/multiple_choice/answering_instructions.jinja +0 -9
- edsl/questions/templates/multiple_choice/html.jinja +0 -0
- edsl/questions/templates/multiple_choice/question_presentation.jinja +0 -12
- edsl/questions/templates/numerical/__init__.py +0 -0
- edsl/questions/templates/numerical/answering_instructions.jinja +0 -8
- edsl/questions/templates/numerical/question_presentation.jinja +0 -7
- edsl/questions/templates/rank/__init__.py +0 -0
- edsl/questions/templates/rank/answering_instructions.jinja +0 -11
- edsl/questions/templates/rank/question_presentation.jinja +0 -15
- edsl/questions/templates/top_k/__init__.py +0 -0
- edsl/questions/templates/top_k/answering_instructions.jinja +0 -8
- edsl/questions/templates/top_k/question_presentation.jinja +0 -22
- edsl/questions/templates/yes_no/__init__.py +0 -0
- edsl/questions/templates/yes_no/answering_instructions.jinja +0 -6
- edsl/questions/templates/yes_no/question_presentation.jinja +0 -12
- edsl/results/DatasetTree.py +0 -145
- edsl/results/Selector.py +0 -118
- edsl/results/tree_explore.py +0 -115
- edsl/surveys/instructions/ChangeInstruction.py +0 -47
- edsl/surveys/instructions/Instruction.py +0 -34
- edsl/surveys/instructions/InstructionCollection.py +0 -77
- edsl/surveys/instructions/__init__.py +0 -0
- edsl/templates/error_reporting/base.html +0 -24
- edsl/templates/error_reporting/exceptions_by_model.html +0 -35
- edsl/templates/error_reporting/exceptions_by_question_name.html +0 -17
- edsl/templates/error_reporting/exceptions_by_type.html +0 -17
- edsl/templates/error_reporting/interview_details.html +0 -116
- edsl/templates/error_reporting/interviews.html +0 -10
- edsl/templates/error_reporting/overview.html +0 -5
- edsl/templates/error_reporting/performance_plot.html +0 -2
- edsl/templates/error_reporting/report.css +0 -74
- edsl/templates/error_reporting/report.html +0 -118
- edsl/templates/error_reporting/report.js +0 -25
- edsl-0.1.33.dist-info/RECORD +0 -295
- {edsl-0.1.33.dist-info → edsl-0.1.33.dev1.dist-info}/LICENSE +0 -0
- {edsl-0.1.33.dist-info → edsl-0.1.33.dev1.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
|
4
5
|
from abc import ABC, abstractmethod
|
5
|
-
from typing import Any, Type, Optional, List, Callable
|
6
|
+
from typing import Any, Type, Optional, List, Callable
|
6
7
|
import copy
|
7
8
|
|
8
9
|
from edsl.exceptions import (
|
9
10
|
QuestionResponseValidationError,
|
10
|
-
QuestionAnswerValidationError,
|
11
11
|
QuestionSerializationError,
|
12
12
|
)
|
13
13
|
from edsl.questions.descriptors import QuestionNameDescriptor, QuestionTextDescriptor
|
@@ -19,8 +19,6 @@ 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
|
24
22
|
from edsl.utilities.decorators import add_edsl_version, remove_edsl_version
|
25
23
|
|
26
24
|
|
@@ -28,128 +26,80 @@ class QuestionBase(
|
|
28
26
|
PersistenceMixin,
|
29
27
|
RichPrintingMixin,
|
30
28
|
SimpleAskMixin,
|
31
|
-
QuestionBasePromptsMixin,
|
32
|
-
QuestionBaseGenMixin,
|
33
29
|
ABC,
|
34
30
|
AnswerValidatorMixin,
|
35
31
|
metaclass=RegisterQuestionsMeta,
|
36
32
|
):
|
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
|
-
"""
|
33
|
+
"""ABC for the Question class. All questions should inherit from this class."""
|
40
34
|
|
41
35
|
question_name: str = QuestionNameDescriptor()
|
42
36
|
question_text: str = QuestionTextDescriptor()
|
43
37
|
|
44
|
-
|
45
|
-
|
46
|
-
|
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)
|
60
|
-
|
61
|
-
@property
|
62
|
-
def validator_parameters(self) -> list[str]:
|
63
|
-
"""Return the parameters required for the response validator.
|
38
|
+
def __getitem__(self, key: str) -> Any:
|
39
|
+
"""Get an attribute of the question."""
|
40
|
+
return getattr(self, key)
|
64
41
|
|
65
|
-
|
66
|
-
|
67
|
-
|
42
|
+
def __hash__(self) -> int:
|
43
|
+
"""Return a hash of the question."""
|
44
|
+
from edsl.utilities.utilities import dict_hash
|
68
45
|
|
69
|
-
|
70
|
-
return self.response_validator_class.required_params
|
46
|
+
return dict_hash(self._to_dict())
|
71
47
|
|
72
|
-
|
73
|
-
|
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
|
-
"""
|
48
|
+
def _repr_html_(self):
|
49
|
+
from edsl.utilities.utilities import data_to_html
|
109
50
|
|
110
|
-
|
51
|
+
data = self.to_dict()
|
52
|
+
try:
|
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.")
|
111
57
|
|
112
|
-
|
58
|
+
return data_to_html(data)
|
113
59
|
|
114
|
-
|
115
|
-
|
116
|
-
def name(self) -> str:
|
117
|
-
"Helper function so questions and instructions can use the same access method"
|
118
|
-
return self.question_name
|
60
|
+
def apply_function(self, func: Callable, exclude_components=None) -> QuestionBase:
|
61
|
+
"""Apply a function to the question parts
|
119
62
|
|
120
|
-
|
121
|
-
"""
|
63
|
+
>>> from edsl.questions import QuestionFreeText
|
64
|
+
>>> q = QuestionFreeText(question_name = "color", question_text = "What is your favorite color?")
|
65
|
+
>>> shouting = lambda x: x.upper()
|
66
|
+
>>> q.apply_function(shouting)
|
67
|
+
Question('free_text', question_name = \"""color\""", question_text = \"""WHAT IS YOUR FAVORITE COLOR?\""")
|
122
68
|
|
123
|
-
>>> from edsl import QuestionFreeText as Q
|
124
|
-
>>> hash(Q.example())
|
125
|
-
1144312636257752766
|
126
69
|
"""
|
127
|
-
|
128
|
-
|
129
|
-
|
70
|
+
if exclude_components is None:
|
71
|
+
exclude_components = ["question_name", "question_type"]
|
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)
|
130
88
|
|
131
89
|
@property
|
132
90
|
def data(self) -> dict:
|
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
|
-
]
|
91
|
+
"""Return a dictionary of question attributes **except** for question_type."""
|
148
92
|
candidate_data = {
|
149
93
|
k.replace("_", "", 1): v
|
150
94
|
for k, v in self.__dict__.items()
|
151
|
-
if k.startswith("_")
|
95
|
+
if k.startswith("_")
|
152
96
|
}
|
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)
|
153
103
|
|
154
104
|
if "func" in candidate_data:
|
155
105
|
func = candidate_data.pop("func")
|
@@ -159,22 +109,147 @@ class QuestionBase(
|
|
159
109
|
|
160
110
|
return candidate_data
|
161
111
|
|
162
|
-
|
163
|
-
|
112
|
+
@classmethod
|
113
|
+
def applicable_prompts(
|
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.
|
164
125
|
|
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'}
|
167
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.
|
207
|
+
|
208
|
+
>>> from edsl import QuestionFreeText
|
209
|
+
>>> QuestionFreeText.example().get_instructions()
|
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
|
+
\""")
|
214
|
+
"""
|
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)."""
|
168
246
|
candidate_data = self.data.copy()
|
169
247
|
candidate_data["question_type"] = self.question_type
|
170
248
|
return candidate_data
|
171
249
|
|
172
250
|
@add_edsl_version
|
173
251
|
def to_dict(self) -> dict[str, Any]:
|
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
|
-
"""
|
252
|
+
"""Convert the question to a dictionary that includes the question type (used in deserialization)."""
|
178
253
|
return self._to_dict()
|
179
254
|
|
180
255
|
@classmethod
|
@@ -214,90 +289,39 @@ class QuestionBase(
|
|
214
289
|
|
215
290
|
return question_class(**local_data)
|
216
291
|
|
217
|
-
|
218
|
-
|
219
|
-
|
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
|
224
|
-
|
225
|
-
return LanguageModel.example(canned_response=canned_response, test_model=True)
|
292
|
+
def copy(self) -> Type[QuestionBase]:
|
293
|
+
"""Return a deep copy of the question."""
|
294
|
+
return copy.deepcopy(self)
|
226
295
|
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
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
|
296
|
+
############################
|
297
|
+
# Dunder methods
|
298
|
+
############################
|
299
|
+
def print(self):
|
300
|
+
from rich import print_json
|
301
|
+
import json
|
251
302
|
|
252
|
-
|
253
|
-
results = cls.example(**kwargs).by(model).run(cache=cache)
|
254
|
-
if show_answer:
|
255
|
-
results.select("answer.*").print()
|
256
|
-
else:
|
257
|
-
return results
|
303
|
+
print_json(json.dumps(self.to_dict()))
|
258
304
|
|
259
305
|
def __call__(self, just_answer=True, model=None, agent=None, **kwargs):
|
260
306
|
"""Call the question.
|
261
307
|
|
262
|
-
|
263
|
-
>>>
|
264
|
-
>>>
|
265
|
-
>>> q =
|
308
|
+
>>> from edsl.language_models import LanguageModel
|
309
|
+
>>> m = LanguageModel.example(canned_response = "Yo, what's up?", test_model = True)
|
310
|
+
>>> from edsl import QuestionFreeText
|
311
|
+
>>> q = QuestionFreeText(question_name = "color", question_text = "What is your favorite color?")
|
266
312
|
>>> q(model = m)
|
267
313
|
"Yo, what's up?"
|
268
314
|
|
269
315
|
"""
|
270
316
|
survey = self.to_survey()
|
271
|
-
results = survey(model=model, agent=agent, **kwargs
|
317
|
+
results = survey(model=model, agent=agent, **kwargs)
|
272
318
|
if just_answer:
|
273
319
|
return results.select(f"answer.{self.question_name}").first()
|
274
320
|
else:
|
275
321
|
return results
|
276
322
|
|
277
|
-
def
|
278
|
-
"""
|
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
|
-
"""
|
323
|
+
async def run_async(self, just_answer=True, model=None, agent=None, **kwargs):
|
324
|
+
"""Call the question."""
|
301
325
|
survey = self.to_survey()
|
302
326
|
results = await survey.run_async(model=model, agent=agent, **kwargs)
|
303
327
|
if just_answer:
|
@@ -305,37 +329,9 @@ class QuestionBase(
|
|
305
329
|
else:
|
306
330
|
return results
|
307
331
|
|
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
|
-
|
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
|
-
"""
|
333
|
+
"""Return a string representation of the question. Should be able to be used to reconstruct the question."""
|
334
|
+
class_name = self.__class__.__name__
|
339
335
|
items = [
|
340
336
|
f'{k} = """{v}"""' if isinstance(v, str) else f"{k} = {v}"
|
341
337
|
for k, v in self.data.items()
|
@@ -344,31 +340,14 @@ class QuestionBase(
|
|
344
340
|
question_type = self.to_dict().get("question_type", "None")
|
345
341
|
return f"Question('{question_type}', {', '.join(items)})"
|
346
342
|
|
347
|
-
def __eq__(self, other:
|
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
|
-
"""
|
343
|
+
def __eq__(self, other: Type[QuestionBase]) -> bool:
|
344
|
+
"""Check if two questions are equal. Equality is defined as having the .to_dict()."""
|
360
345
|
if not isinstance(other, QuestionBase):
|
361
346
|
return False
|
362
347
|
return self.to_dict() == other.to_dict()
|
363
348
|
|
364
349
|
def __sub__(self, other) -> BaseDiff:
|
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
|
-
"""
|
350
|
+
"""Return the difference between two objects."""
|
372
351
|
|
373
352
|
return BaseDiff(other, self)
|
374
353
|
|
@@ -385,51 +364,57 @@ class QuestionBase(
|
|
385
364
|
):
|
386
365
|
return other_question_or_diff.apply(self)
|
387
366
|
|
388
|
-
|
389
|
-
# return compose_questions(self, other_question_or_diff)
|
367
|
+
from edsl.questions import compose_questions
|
390
368
|
|
391
|
-
|
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
|
369
|
+
return compose_questions(self, other_question_or_diff)
|
398
370
|
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
return answer
|
371
|
+
@abstractmethod
|
372
|
+
def _validate_answer(self, answer: dict[str, str]):
|
373
|
+
"""Validate the answer from the LLM. Behavior depends on the question type."""
|
374
|
+
pass
|
404
375
|
|
405
|
-
|
376
|
+
def _validate_response(self, response):
|
377
|
+
"""Validate the response from the LLM. Behavior depends on the question type."""
|
378
|
+
if "answer" not in response:
|
379
|
+
raise QuestionResponseValidationError(
|
380
|
+
"Response from LLM does not have an answer"
|
381
|
+
)
|
382
|
+
return response
|
406
383
|
|
407
|
-
|
408
|
-
def
|
409
|
-
"""
|
384
|
+
@abstractmethod
|
385
|
+
def _translate_answer_code_to_answer(self): # pragma: no cover
|
386
|
+
"""Translate the answer code to the actual answer. Behavior depends on the question type."""
|
387
|
+
pass
|
410
388
|
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
389
|
+
@abstractmethod
|
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
|
393
|
+
|
394
|
+
############################
|
395
|
+
# Forward methods
|
396
|
+
############################
|
397
|
+
def add_question(self, other: QuestionBase) -> "Survey":
|
398
|
+
"""Add a question to this question by turning them into a survey with two questions."""
|
417
399
|
from edsl.surveys.Survey import Survey
|
418
400
|
|
419
401
|
s = Survey([self, other])
|
420
402
|
return s
|
421
403
|
|
422
404
|
def to_survey(self) -> "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
|
-
"""
|
405
|
+
"""Turn a single question into a survey."""
|
428
406
|
from edsl.surveys.Survey import Survey
|
429
407
|
|
430
408
|
s = Survey([self])
|
431
409
|
return s
|
432
410
|
|
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
|
+
|
433
418
|
def by(self, *args) -> "Jobs":
|
434
419
|
"""Turn a single question into a survey and then a Job."""
|
435
420
|
from edsl.surveys.Survey import Survey
|
@@ -437,15 +422,6 @@ class QuestionBase(
|
|
437
422
|
s = Survey([self])
|
438
423
|
return s.by(*args)
|
439
424
|
|
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
|
-
|
449
425
|
def human_readable(self) -> str:
|
450
426
|
"""Print the question in a human readable format.
|
451
427
|
|
@@ -466,7 +442,6 @@ class QuestionBase(
|
|
466
442
|
self,
|
467
443
|
scenario: Optional[dict] = None,
|
468
444
|
agent: Optional[dict] = {},
|
469
|
-
answers: Optional[dict] = None,
|
470
445
|
include_question_name: bool = False,
|
471
446
|
height: Optional[int] = None,
|
472
447
|
width: Optional[int] = None,
|
@@ -478,17 +453,6 @@ class QuestionBase(
|
|
478
453
|
if scenario is None:
|
479
454
|
scenario = {}
|
480
455
|
|
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
|
-
|
492
456
|
base_template = """
|
493
457
|
<div id="{{ question_name }}" class="survey_question" data-type="{{ question_type }}">
|
494
458
|
{% if include_question_name %}
|
@@ -508,40 +472,13 @@ class QuestionBase(
|
|
508
472
|
|
509
473
|
base_template = Template(base_template)
|
510
474
|
|
511
|
-
|
512
|
-
"
|
513
|
-
"
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
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
|
-
)
|
475
|
+
params = {
|
476
|
+
"question_name": self.question_name,
|
477
|
+
"question_text": Template(self.question_text).render(scenario, agent=agent),
|
478
|
+
"question_type": self.question_type,
|
479
|
+
"question_content": Template(question_content).render(scenario),
|
480
|
+
"include_question_name": include_question_name,
|
481
|
+
}
|
545
482
|
rendered_html = base_template.render(**params)
|
546
483
|
|
547
484
|
if iframe:
|
@@ -560,21 +497,6 @@ class QuestionBase(
|
|
560
497
|
|
561
498
|
return rendered_html
|
562
499
|
|
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
|
-
|
578
500
|
def rich_print(self):
|
579
501
|
"""Print the question in a rich format."""
|
580
502
|
from rich.table import Table
|
@@ -598,8 +520,6 @@ class QuestionBase(
|
|
598
520
|
)
|
599
521
|
return table
|
600
522
|
|
601
|
-
# endregion
|
602
|
-
|
603
523
|
|
604
524
|
if __name__ == "__main__":
|
605
525
|
import doctest
|