edsl 0.1.33.dev1__py3-none-any.whl → 0.1.33.dev2__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/TemplateLoader.py +24 -0
- edsl/__init__.py +8 -4
- edsl/agents/Agent.py +46 -14
- edsl/agents/AgentList.py +43 -0
- edsl/agents/Invigilator.py +125 -212
- edsl/agents/InvigilatorBase.py +140 -32
- edsl/agents/PromptConstructionMixin.py +43 -66
- 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 +38 -39
- edsl/coop/PriceFetcher.py +58 -0
- edsl/coop/coop.py +39 -5
- edsl/data/Cache.py +35 -1
- edsl/data_transfer_models.py +120 -38
- edsl/enums.py +2 -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 +24 -0
- edsl/inference_services/MistralAIService.py +120 -0
- edsl/inference_services/OpenAIService.py +41 -50
- edsl/inference_services/TestService.py +71 -0
- edsl/inference_services/models_available_cache.py +0 -6
- edsl/inference_services/registry.py +4 -0
- edsl/jobs/Answers.py +10 -12
- edsl/jobs/FailedQuestion.py +78 -0
- edsl/jobs/Jobs.py +18 -13
- edsl/jobs/buckets/TokenBucket.py +39 -14
- edsl/jobs/interviews/Interview.py +297 -77
- edsl/jobs/interviews/InterviewExceptionEntry.py +83 -19
- edsl/jobs/interviews/interview_exception_tracking.py +0 -70
- edsl/jobs/interviews/retry_management.py +3 -1
- edsl/jobs/runners/JobsRunnerAsyncio.py +116 -70
- edsl/jobs/runners/JobsRunnerStatusMixin.py +1 -1
- edsl/jobs/tasks/QuestionTaskCreator.py +30 -23
- edsl/jobs/tasks/TaskHistory.py +131 -213
- edsl/language_models/LanguageModel.py +239 -129
- 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 +15 -2
- edsl/language_models/repair.py +0 -19
- edsl/language_models/utilities.py +61 -0
- edsl/prompts/Prompt.py +52 -2
- edsl/questions/AnswerValidatorMixin.py +23 -26
- edsl/questions/QuestionBase.py +273 -242
- edsl/questions/QuestionBaseGenMixin.py +133 -0
- edsl/questions/QuestionBasePromptsMixin.py +266 -0
- edsl/questions/QuestionBudget.py +6 -0
- edsl/questions/QuestionCheckBox.py +227 -35
- edsl/questions/QuestionExtract.py +98 -27
- edsl/questions/QuestionFreeText.py +46 -29
- edsl/questions/QuestionFunctional.py +7 -0
- edsl/questions/QuestionList.py +141 -22
- edsl/questions/QuestionMultipleChoice.py +173 -64
- edsl/questions/QuestionNumerical.py +87 -46
- edsl/questions/QuestionRank.py +182 -24
- edsl/questions/RegisterQuestionsMeta.py +31 -12
- edsl/questions/ResponseValidatorABC.py +169 -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 +11 -1
- edsl/questions/derived/QuestionTopK.py +6 -0
- edsl/questions/derived/QuestionYesNo.py +16 -1
- 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/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/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/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 +41 -47
- edsl/results/DatasetTree.py +145 -0
- edsl/results/Result.py +32 -5
- edsl/results/Results.py +131 -45
- edsl/results/ResultsDBMixin.py +3 -3
- edsl/results/Selector.py +118 -0
- edsl/results/tree_explore.py +115 -0
- edsl/scenarios/Scenario.py +10 -4
- edsl/scenarios/ScenarioList.py +348 -39
- edsl/scenarios/ScenarioListExportMixin.py +9 -0
- edsl/study/SnapShot.py +8 -1
- edsl/surveys/RuleCollection.py +2 -2
- edsl/surveys/Survey.py +634 -315
- 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 +111 -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-0.1.33.dev1.dist-info → edsl-0.1.33.dev2.dist-info}/METADATA +4 -2
- edsl-0.1.33.dev2.dist-info/RECORD +289 -0
- edsl/jobs/interviews/InterviewTaskBuildingMixin.py +0 -286
- edsl/utilities/gcp_bucket/simple_example.py +0 -9
- edsl-0.1.33.dev1.dist-info/RECORD +0 -209
- {edsl-0.1.33.dev1.dist-info → edsl-0.1.33.dev2.dist-info}/LICENSE +0 -0
- {edsl-0.1.33.dev1.dist-info → edsl-0.1.33.dev2.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,133 @@ 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'})
|
107
|
+
{'answer': 'Hello', 'generated_tokens': None}
|
108
|
+
>>> Q.example()._validate_answer({'shmanswer': 1})
|
109
|
+
Traceback (most recent call last):
|
110
|
+
...
|
111
|
+
edsl.exceptions.questions.QuestionAnswerValidationError:...
|
112
|
+
...
|
113
|
+
"""
|
59
114
|
|
60
|
-
|
61
|
-
"""Apply a function to the question parts
|
115
|
+
return self.response_validator.validate(answer)
|
62
116
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
117
|
+
# endregion
|
118
|
+
|
119
|
+
# region: Serialization methods
|
120
|
+
@property
|
121
|
+
def name(self) -> str:
|
122
|
+
"Helper function so questions and instructions can use the same access method"
|
123
|
+
return self.question_name
|
68
124
|
|
125
|
+
def __hash__(self) -> int:
|
126
|
+
"""Return a hash of the question.
|
127
|
+
|
128
|
+
>>> from edsl import QuestionFreeText as Q
|
129
|
+
>>> hash(Q.example())
|
130
|
+
1144312636257752766
|
69
131
|
"""
|
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)
|
132
|
+
from edsl.utilities.utilities import dict_hash
|
133
|
+
|
134
|
+
return dict_hash(self._to_dict())
|
88
135
|
|
89
136
|
@property
|
90
137
|
def data(self) -> dict:
|
91
|
-
"""Return a dictionary of question attributes **except** for question_type.
|
138
|
+
"""Return a dictionary of question attributes **except** for question_type.
|
139
|
+
|
140
|
+
>>> from edsl import QuestionFreeText as Q
|
141
|
+
>>> Q.example().data
|
142
|
+
{'question_name': 'how_are_you', 'question_text': 'How are you?'}
|
143
|
+
"""
|
144
|
+
exclude_list = [
|
145
|
+
"question_type",
|
146
|
+
"_include_comment",
|
147
|
+
"_fake_data_factory",
|
148
|
+
"_use_code",
|
149
|
+
"_answering_instructions",
|
150
|
+
"_question_presentation",
|
151
|
+
"_model_instructions",
|
152
|
+
]
|
92
153
|
candidate_data = {
|
93
154
|
k.replace("_", "", 1): v
|
94
155
|
for k, v in self.__dict__.items()
|
95
|
-
if k.startswith("_")
|
96
|
-
}
|
97
|
-
optional_attributes = {
|
98
|
-
"set_instructions": "instructions",
|
156
|
+
if k.startswith("_") and k not in exclude_list
|
99
157
|
}
|
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
158
|
|
104
159
|
if "func" in candidate_data:
|
105
160
|
func = candidate_data.pop("func")
|
@@ -109,147 +164,22 @@ class QuestionBase(
|
|
109
164
|
|
110
165
|
return candidate_data
|
111
166
|
|
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.
|
167
|
+
def _to_dict(self):
|
168
|
+
"""Convert the question to a dictionary that includes the question type (used in deserialization).
|
207
169
|
|
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
|
-
\""")
|
170
|
+
>>> from edsl import QuestionFreeText as Q; Q.example()._to_dict()
|
171
|
+
{'question_name': 'how_are_you', 'question_text': 'How are you?', 'question_type': 'free_text'}
|
214
172
|
"""
|
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
173
|
candidate_data = self.data.copy()
|
247
174
|
candidate_data["question_type"] = self.question_type
|
248
175
|
return candidate_data
|
249
176
|
|
250
177
|
@add_edsl_version
|
251
178
|
def to_dict(self) -> dict[str, Any]:
|
252
|
-
"""Convert the question to a dictionary that includes the question type (used in deserialization).
|
179
|
+
"""Convert the question to a dictionary that includes the question type (used in deserialization).
|
180
|
+
>>> from edsl import QuestionFreeText as Q; Q.example().to_dict()
|
181
|
+
{'question_name': 'how_are_you', 'question_text': 'How are you?', 'question_type': 'free_text', 'edsl_version': '...'}
|
182
|
+
"""
|
253
183
|
return self._to_dict()
|
254
184
|
|
255
185
|
@classmethod
|
@@ -289,39 +219,90 @@ class QuestionBase(
|
|
289
219
|
|
290
220
|
return question_class(**local_data)
|
291
221
|
|
292
|
-
|
293
|
-
"""Return a deep copy of the question."""
|
294
|
-
return copy.deepcopy(self)
|
222
|
+
# endregion
|
295
223
|
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
from
|
301
|
-
import json
|
224
|
+
# region: Running methods
|
225
|
+
@classmethod
|
226
|
+
def _get_test_model(self, canned_response: Optional[str] = None) -> "LanguageModel":
|
227
|
+
"""Get a test model for the question."""
|
228
|
+
from edsl.language_models import LanguageModel
|
302
229
|
|
303
|
-
|
230
|
+
return LanguageModel.example(canned_response=canned_response, test_model=True)
|
231
|
+
|
232
|
+
@classmethod
|
233
|
+
def run_example(
|
234
|
+
cls,
|
235
|
+
show_answer: bool = True,
|
236
|
+
model: Optional["LanguageModel"] = None,
|
237
|
+
cache=False,
|
238
|
+
**kwargs,
|
239
|
+
):
|
240
|
+
"""Run an example of the question.
|
241
|
+
>>> from edsl.language_models import LanguageModel
|
242
|
+
>>> from edsl import QuestionFreeText as Q
|
243
|
+
>>> m = Q._get_test_model(canned_response = "Yo, what's up?")
|
244
|
+
>>> m.execute_model_call("", "")
|
245
|
+
{'message': [{'text': "Yo, what's up?"}], 'usage': {'prompt_tokens': 1, 'completion_tokens': 1}}
|
246
|
+
>>> Q.run_example(show_answer = True, model = m)
|
247
|
+
┏━━━━━━━━━━━━━━━━┓
|
248
|
+
┃ answer ┃
|
249
|
+
┃ .how_are_you ┃
|
250
|
+
┡━━━━━━━━━━━━━━━━┩
|
251
|
+
│ Yo, what's up? │
|
252
|
+
└────────────────┘
|
253
|
+
"""
|
254
|
+
if model is None:
|
255
|
+
from edsl import Model
|
256
|
+
|
257
|
+
model = Model()
|
258
|
+
results = cls.example(**kwargs).by(model).run(cache=cache)
|
259
|
+
if show_answer:
|
260
|
+
results.select("answer.*").print()
|
261
|
+
else:
|
262
|
+
return results
|
304
263
|
|
305
264
|
def __call__(self, just_answer=True, model=None, agent=None, **kwargs):
|
306
265
|
"""Call the question.
|
307
266
|
|
308
|
-
|
309
|
-
>>>
|
310
|
-
>>>
|
311
|
-
>>> q =
|
267
|
+
|
268
|
+
>>> from edsl import QuestionFreeText as Q
|
269
|
+
>>> m = Q._get_test_model(canned_response = "Yo, what's up?")
|
270
|
+
>>> q = Q(question_name = "color", question_text = "What is your favorite color?")
|
312
271
|
>>> q(model = m)
|
313
272
|
"Yo, what's up?"
|
314
273
|
|
315
274
|
"""
|
316
275
|
survey = self.to_survey()
|
317
|
-
results = survey(model=model, agent=agent, **kwargs)
|
276
|
+
results = survey(model=model, agent=agent, **kwargs, cache=False)
|
318
277
|
if just_answer:
|
319
278
|
return results.select(f"answer.{self.question_name}").first()
|
320
279
|
else:
|
321
280
|
return results
|
322
281
|
|
323
|
-
|
324
|
-
"""
|
282
|
+
def run(self, *args, **kwargs) -> "Results":
|
283
|
+
"""Turn a single question into a survey and runs it."""
|
284
|
+
from edsl.surveys.Survey import Survey
|
285
|
+
|
286
|
+
s = self.to_survey()
|
287
|
+
return s.run(*args, **kwargs)
|
288
|
+
|
289
|
+
async def run_async(
|
290
|
+
self,
|
291
|
+
just_answer: bool = True,
|
292
|
+
model: Optional["Model"] = None,
|
293
|
+
agent: Optional["Agent"] = None,
|
294
|
+
**kwargs,
|
295
|
+
) -> Union[Any, "Results"]:
|
296
|
+
"""Call the question asynchronously.
|
297
|
+
|
298
|
+
>>> import asyncio
|
299
|
+
>>> from edsl import QuestionFreeText as Q
|
300
|
+
>>> m = Q._get_test_model(canned_response = "Blue")
|
301
|
+
>>> q = Q(question_name = "color", question_text = "What is your favorite color?")
|
302
|
+
>>> async def test_run_async(): result = await q.run_async(model=m); print(result)
|
303
|
+
>>> asyncio.run(test_run_async())
|
304
|
+
Blue
|
305
|
+
"""
|
325
306
|
survey = self.to_survey()
|
326
307
|
results = await survey.run_async(model=model, agent=agent, **kwargs)
|
327
308
|
if just_answer:
|
@@ -329,9 +310,37 @@ class QuestionBase(
|
|
329
310
|
else:
|
330
311
|
return results
|
331
312
|
|
313
|
+
# endregion
|
314
|
+
|
315
|
+
# region: Magic methods
|
316
|
+
def _repr_html_(self):
|
317
|
+
from edsl.utilities.utilities import data_to_html
|
318
|
+
|
319
|
+
data = self.to_dict()
|
320
|
+
try:
|
321
|
+
_ = data.pop("edsl_version")
|
322
|
+
_ = data.pop("edsl_class_name")
|
323
|
+
except KeyError:
|
324
|
+
print("Serialized question lacks edsl version, but is should have it.")
|
325
|
+
|
326
|
+
return data_to_html(data)
|
327
|
+
|
328
|
+
def __getitem__(self, key: str) -> Any:
|
329
|
+
"""Get an attribute of the question so it can be treated like a dictionary.
|
330
|
+
|
331
|
+
>>> from edsl.questions import QuestionFreeText as Q
|
332
|
+
>>> Q.example()['question_text']
|
333
|
+
'How are you?'
|
334
|
+
"""
|
335
|
+
return getattr(self, key)
|
336
|
+
|
332
337
|
def __repr__(self) -> str:
|
333
|
-
"""Return a string representation of the question. Should be able to be used to reconstruct the question.
|
334
|
-
|
338
|
+
"""Return a string representation of the question. Should be able to be used to reconstruct the question.
|
339
|
+
|
340
|
+
>>> from edsl import QuestionFreeText as Q
|
341
|
+
>>> repr(Q.example())
|
342
|
+
'Question(\\'free_text\\', question_name = \"""how_are_you\""", question_text = \"""How are you?\""")'
|
343
|
+
"""
|
335
344
|
items = [
|
336
345
|
f'{k} = """{v}"""' if isinstance(v, str) else f"{k} = {v}"
|
337
346
|
for k, v in self.data.items()
|
@@ -340,14 +349,31 @@ class QuestionBase(
|
|
340
349
|
question_type = self.to_dict().get("question_type", "None")
|
341
350
|
return f"Question('{question_type}', {', '.join(items)})"
|
342
351
|
|
343
|
-
def __eq__(self, other: Type[QuestionBase]) -> bool:
|
344
|
-
"""Check if two questions are equal. Equality is defined as having the .to_dict().
|
352
|
+
def __eq__(self, other: Union[Any, Type[QuestionBase]]) -> bool:
|
353
|
+
"""Check if two questions are equal. Equality is defined as having the .to_dict().
|
354
|
+
|
355
|
+
>>> from edsl import QuestionFreeText as Q
|
356
|
+
>>> q1 = Q.example()
|
357
|
+
>>> q2 = Q.example()
|
358
|
+
>>> q1 == q2
|
359
|
+
True
|
360
|
+
>>> q1.question_text = "How are you John?"
|
361
|
+
>>> q1 == q2
|
362
|
+
False
|
363
|
+
|
364
|
+
"""
|
345
365
|
if not isinstance(other, QuestionBase):
|
346
366
|
return False
|
347
367
|
return self.to_dict() == other.to_dict()
|
348
368
|
|
349
369
|
def __sub__(self, other) -> BaseDiff:
|
350
|
-
"""Return the difference between two objects.
|
370
|
+
"""Return the difference between two objects.
|
371
|
+
>>> from edsl import QuestionFreeText as Q
|
372
|
+
>>> q1 = Q.example()
|
373
|
+
>>> q2 = q1.copy()
|
374
|
+
>>> q2.question_text = "How are you John?"
|
375
|
+
>>> diff = q1 - q2
|
376
|
+
"""
|
351
377
|
|
352
378
|
return BaseDiff(other, self)
|
353
379
|
|
@@ -364,57 +390,51 @@ class QuestionBase(
|
|
364
390
|
):
|
365
391
|
return other_question_or_diff.apply(self)
|
366
392
|
|
367
|
-
from edsl.questions import compose_questions
|
368
|
-
|
369
|
-
return compose_questions(self, other_question_or_diff)
|
393
|
+
# from edsl.questions import compose_questions
|
394
|
+
# return compose_questions(self, other_question_or_diff)
|
370
395
|
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
396
|
+
# def _validate_response(self, response):
|
397
|
+
# """Validate the response from the LLM. Behavior depends on the question type."""
|
398
|
+
# if "answer" not in response:
|
399
|
+
# raise QuestionResponseValidationError(
|
400
|
+
# "Response from LLM does not have an answer"
|
401
|
+
# )
|
402
|
+
# return response
|
375
403
|
|
376
|
-
def
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
)
|
382
|
-
return response
|
383
|
-
|
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
|
404
|
+
def _translate_answer_code_to_answer(
|
405
|
+
self, answer, scenario: Optional["Scenario"] = None
|
406
|
+
):
|
407
|
+
"""There is over-ridden by child classes that ask for codes."""
|
408
|
+
return answer
|
388
409
|
|
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
|
410
|
+
# endregion
|
393
411
|
|
394
|
-
|
395
|
-
# Forward methods
|
396
|
-
############################
|
412
|
+
# region: Forward methods
|
397
413
|
def add_question(self, other: QuestionBase) -> "Survey":
|
398
|
-
"""Add a question to this question by turning them into a survey with two questions.
|
414
|
+
"""Add a question to this question by turning them into a survey with two questions.
|
415
|
+
|
416
|
+
>>> from edsl.questions import QuestionFreeText as Q
|
417
|
+
>>> from edsl.questions import QuestionMultipleChoice as QMC
|
418
|
+
>>> s = Q.example().add_question(QMC.example())
|
419
|
+
>>> len(s.questions)
|
420
|
+
2
|
421
|
+
"""
|
399
422
|
from edsl.surveys.Survey import Survey
|
400
423
|
|
401
424
|
s = Survey([self, other])
|
402
425
|
return s
|
403
426
|
|
404
427
|
def to_survey(self) -> "Survey":
|
405
|
-
"""Turn a single question into a survey.
|
428
|
+
"""Turn a single question into a survey.
|
429
|
+
>>> from edsl import QuestionFreeText as Q
|
430
|
+
>>> Q.example().to_survey().questions[0].question_name
|
431
|
+
'how_are_you'
|
432
|
+
"""
|
406
433
|
from edsl.surveys.Survey import Survey
|
407
434
|
|
408
435
|
s = Survey([self])
|
409
436
|
return s
|
410
437
|
|
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
438
|
def by(self, *args) -> "Jobs":
|
419
439
|
"""Turn a single question into a survey and then a Job."""
|
420
440
|
from edsl.surveys.Survey import Survey
|
@@ -422,6 +442,15 @@ class QuestionBase(
|
|
422
442
|
s = Survey([self])
|
423
443
|
return s.by(*args)
|
424
444
|
|
445
|
+
# endregion
|
446
|
+
|
447
|
+
# region: Display methods
|
448
|
+
def print(self):
|
449
|
+
from rich import print_json
|
450
|
+
import json
|
451
|
+
|
452
|
+
print_json(json.dumps(self.to_dict()))
|
453
|
+
|
425
454
|
def human_readable(self) -> str:
|
426
455
|
"""Print the question in a human readable format.
|
427
456
|
|
@@ -520,6 +549,8 @@ class QuestionBase(
|
|
520
549
|
)
|
521
550
|
return table
|
522
551
|
|
552
|
+
# endregion
|
553
|
+
|
523
554
|
|
524
555
|
if __name__ == "__main__":
|
525
556
|
import doctest
|