edsl 0.1.33.dev1__py3-none-any.whl → 0.1.33.dev3__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 +74 -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 +36 -21
- 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 +154 -183
- 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/prompts/Prompt.py +52 -2
- edsl/questions/AnswerValidatorMixin.py +23 -26
- edsl/questions/QuestionBase.py +328 -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 +162 -68
- 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.33.dev1.dist-info → edsl-0.1.33.dev3.dist-info}/METADATA +5 -2
- edsl-0.1.33.dev3.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.33.dev1.dist-info/RECORD +0 -209
- {edsl-0.1.33.dev1.dist-info → edsl-0.1.33.dev3.dist-info}/LICENSE +0 -0
- {edsl-0.1.33.dev1.dist-info → edsl-0.1.33.dev3.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,127 @@ 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
|
59
77
|
|
60
|
-
|
61
|
-
"""Apply a function to the question parts
|
78
|
+
class FakeData(ModelFactory[self.response_model]): ...
|
62
79
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
80
|
+
self._fake_data_factory = FakeData
|
81
|
+
return self._fake_data_factory
|
82
|
+
|
83
|
+
def _simulate_answer(self, human_readable: bool = False) -> dict:
|
84
|
+
"""Simulate a valid answer for debugging purposes (what the validator expects).
|
85
|
+
>>> from edsl import QuestionFreeText as Q
|
86
|
+
>>> Q.example()._simulate_answer()
|
87
|
+
{'answer': '...', 'generated_tokens': ...}
|
88
|
+
"""
|
89
|
+
simulated_answer = self.fake_data_factory.build().dict()
|
90
|
+
if human_readable and hasattr(self, "question_options") and self.use_code:
|
91
|
+
simulated_answer["answer"] = [
|
92
|
+
self.question_options[index] for index in simulated_answer["answer"]
|
93
|
+
]
|
94
|
+
return simulated_answer
|
95
|
+
|
96
|
+
class ValidatedAnswer(TypedDict):
|
97
|
+
answer: Any
|
98
|
+
comment: Optional[str]
|
99
|
+
generated_tokens: Optional[str]
|
100
|
+
|
101
|
+
def _validate_answer(self, answer: dict) -> ValidatedAnswer:
|
102
|
+
"""Validate the answer.
|
103
|
+
>>> from edsl.exceptions import QuestionAnswerValidationError
|
104
|
+
>>> from edsl import QuestionFreeText as Q
|
105
|
+
>>> Q.example()._validate_answer({'answer': 'Hello', 'generated_tokens': 'Hello'})
|
106
|
+
{'answer': 'Hello', 'generated_tokens': 'Hello'}
|
107
|
+
"""
|
108
|
+
|
109
|
+
return self.response_validator.validate(answer)
|
110
|
+
|
111
|
+
# endregion
|
112
|
+
|
113
|
+
# region: Serialization methods
|
114
|
+
@property
|
115
|
+
def name(self) -> str:
|
116
|
+
"Helper function so questions and instructions can use the same access method"
|
117
|
+
return self.question_name
|
118
|
+
|
119
|
+
def __hash__(self) -> int:
|
120
|
+
"""Return a hash of the question.
|
68
121
|
|
122
|
+
>>> from edsl import QuestionFreeText as Q
|
123
|
+
>>> hash(Q.example())
|
124
|
+
1144312636257752766
|
69
125
|
"""
|
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)
|
126
|
+
from edsl.utilities.utilities import dict_hash
|
127
|
+
|
128
|
+
return dict_hash(self._to_dict())
|
88
129
|
|
89
130
|
@property
|
90
131
|
def data(self) -> dict:
|
91
|
-
"""Return a dictionary of question attributes **except** for question_type.
|
132
|
+
"""Return a dictionary of question attributes **except** for question_type.
|
133
|
+
|
134
|
+
>>> from edsl import QuestionFreeText as Q
|
135
|
+
>>> Q.example().data
|
136
|
+
{'question_name': 'how_are_you', 'question_text': 'How are you?'}
|
137
|
+
"""
|
138
|
+
exclude_list = [
|
139
|
+
"question_type",
|
140
|
+
"_include_comment",
|
141
|
+
"_fake_data_factory",
|
142
|
+
"_use_code",
|
143
|
+
"_answering_instructions",
|
144
|
+
"_question_presentation",
|
145
|
+
"_model_instructions",
|
146
|
+
]
|
92
147
|
candidate_data = {
|
93
148
|
k.replace("_", "", 1): v
|
94
149
|
for k, v in self.__dict__.items()
|
95
|
-
if k.startswith("_")
|
96
|
-
}
|
97
|
-
optional_attributes = {
|
98
|
-
"set_instructions": "instructions",
|
150
|
+
if k.startswith("_") and k not in exclude_list
|
99
151
|
}
|
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
152
|
|
104
153
|
if "func" in candidate_data:
|
105
154
|
func = candidate_data.pop("func")
|
@@ -109,147 +158,22 @@ class QuestionBase(
|
|
109
158
|
|
110
159
|
return candidate_data
|
111
160
|
|
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.
|
161
|
+
def _to_dict(self):
|
162
|
+
"""Convert the question to a dictionary that includes the question type (used in deserialization).
|
207
163
|
|
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
|
-
\""")
|
164
|
+
>>> from edsl import QuestionFreeText as Q; Q.example()._to_dict()
|
165
|
+
{'question_name': 'how_are_you', 'question_text': 'How are you?', 'question_type': 'free_text'}
|
214
166
|
"""
|
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
167
|
candidate_data = self.data.copy()
|
247
168
|
candidate_data["question_type"] = self.question_type
|
248
169
|
return candidate_data
|
249
170
|
|
250
171
|
@add_edsl_version
|
251
172
|
def to_dict(self) -> dict[str, Any]:
|
252
|
-
"""Convert the question to a dictionary that includes the question type (used in deserialization).
|
173
|
+
"""Convert the question to a dictionary that includes the question type (used in deserialization).
|
174
|
+
>>> from edsl import QuestionFreeText as Q; Q.example().to_dict()
|
175
|
+
{'question_name': 'how_are_you', 'question_text': 'How are you?', 'question_type': 'free_text', 'edsl_version': '...'}
|
176
|
+
"""
|
253
177
|
return self._to_dict()
|
254
178
|
|
255
179
|
@classmethod
|
@@ -289,39 +213,90 @@ class QuestionBase(
|
|
289
213
|
|
290
214
|
return question_class(**local_data)
|
291
215
|
|
292
|
-
|
293
|
-
"""Return a deep copy of the question."""
|
294
|
-
return copy.deepcopy(self)
|
216
|
+
# endregion
|
295
217
|
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
from
|
301
|
-
import json
|
218
|
+
# region: Running methods
|
219
|
+
@classmethod
|
220
|
+
def _get_test_model(self, canned_response: Optional[str] = None) -> "LanguageModel":
|
221
|
+
"""Get a test model for the question."""
|
222
|
+
from edsl.language_models import LanguageModel
|
302
223
|
|
303
|
-
|
224
|
+
return LanguageModel.example(canned_response=canned_response, test_model=True)
|
225
|
+
|
226
|
+
@classmethod
|
227
|
+
def run_example(
|
228
|
+
cls,
|
229
|
+
show_answer: bool = True,
|
230
|
+
model: Optional["LanguageModel"] = None,
|
231
|
+
cache=False,
|
232
|
+
**kwargs,
|
233
|
+
):
|
234
|
+
"""Run an example of the question.
|
235
|
+
>>> from edsl.language_models import LanguageModel
|
236
|
+
>>> from edsl import QuestionFreeText as Q
|
237
|
+
>>> m = Q._get_test_model(canned_response = "Yo, what's up?")
|
238
|
+
>>> m.execute_model_call("", "")
|
239
|
+
{'message': [{'text': "Yo, what's up?"}], 'usage': {'prompt_tokens': 1, 'completion_tokens': 1}}
|
240
|
+
>>> Q.run_example(show_answer = True, model = m)
|
241
|
+
┏━━━━━━━━━━━━━━━━┓
|
242
|
+
┃ answer ┃
|
243
|
+
┃ .how_are_you ┃
|
244
|
+
┡━━━━━━━━━━━━━━━━┩
|
245
|
+
│ Yo, what's up? │
|
246
|
+
└────────────────┘
|
247
|
+
"""
|
248
|
+
if model is None:
|
249
|
+
from edsl import Model
|
250
|
+
|
251
|
+
model = Model()
|
252
|
+
results = cls.example(**kwargs).by(model).run(cache=cache)
|
253
|
+
if show_answer:
|
254
|
+
results.select("answer.*").print()
|
255
|
+
else:
|
256
|
+
return results
|
304
257
|
|
305
258
|
def __call__(self, just_answer=True, model=None, agent=None, **kwargs):
|
306
259
|
"""Call the question.
|
307
260
|
|
308
|
-
|
309
|
-
>>>
|
310
|
-
>>>
|
311
|
-
>>> q =
|
261
|
+
|
262
|
+
>>> from edsl import QuestionFreeText as Q
|
263
|
+
>>> m = Q._get_test_model(canned_response = "Yo, what's up?")
|
264
|
+
>>> q = Q(question_name = "color", question_text = "What is your favorite color?")
|
312
265
|
>>> q(model = m)
|
313
266
|
"Yo, what's up?"
|
314
267
|
|
315
268
|
"""
|
316
269
|
survey = self.to_survey()
|
317
|
-
results = survey(model=model, agent=agent, **kwargs)
|
270
|
+
results = survey(model=model, agent=agent, **kwargs, cache=False)
|
318
271
|
if just_answer:
|
319
272
|
return results.select(f"answer.{self.question_name}").first()
|
320
273
|
else:
|
321
274
|
return results
|
322
275
|
|
323
|
-
|
324
|
-
"""
|
276
|
+
def run(self, *args, **kwargs) -> "Results":
|
277
|
+
"""Turn a single question into a survey and runs it."""
|
278
|
+
from edsl.surveys.Survey import Survey
|
279
|
+
|
280
|
+
s = self.to_survey()
|
281
|
+
return s.run(*args, **kwargs)
|
282
|
+
|
283
|
+
async def run_async(
|
284
|
+
self,
|
285
|
+
just_answer: bool = True,
|
286
|
+
model: Optional["Model"] = None,
|
287
|
+
agent: Optional["Agent"] = None,
|
288
|
+
**kwargs,
|
289
|
+
) -> Union[Any, "Results"]:
|
290
|
+
"""Call the question asynchronously.
|
291
|
+
|
292
|
+
>>> import asyncio
|
293
|
+
>>> from edsl import QuestionFreeText as Q
|
294
|
+
>>> m = Q._get_test_model(canned_response = "Blue")
|
295
|
+
>>> q = Q(question_name = "color", question_text = "What is your favorite color?")
|
296
|
+
>>> async def test_run_async(): result = await q.run_async(model=m); print(result)
|
297
|
+
>>> asyncio.run(test_run_async())
|
298
|
+
Blue
|
299
|
+
"""
|
325
300
|
survey = self.to_survey()
|
326
301
|
results = await survey.run_async(model=model, agent=agent, **kwargs)
|
327
302
|
if just_answer:
|
@@ -329,9 +304,37 @@ class QuestionBase(
|
|
329
304
|
else:
|
330
305
|
return results
|
331
306
|
|
307
|
+
# endregion
|
308
|
+
|
309
|
+
# region: Magic methods
|
310
|
+
def _repr_html_(self):
|
311
|
+
from edsl.utilities.utilities import data_to_html
|
312
|
+
|
313
|
+
data = self.to_dict()
|
314
|
+
try:
|
315
|
+
_ = data.pop("edsl_version")
|
316
|
+
_ = data.pop("edsl_class_name")
|
317
|
+
except KeyError:
|
318
|
+
print("Serialized question lacks edsl version, but is should have it.")
|
319
|
+
|
320
|
+
return data_to_html(data)
|
321
|
+
|
322
|
+
def __getitem__(self, key: str) -> Any:
|
323
|
+
"""Get an attribute of the question so it can be treated like a dictionary.
|
324
|
+
|
325
|
+
>>> from edsl.questions import QuestionFreeText as Q
|
326
|
+
>>> Q.example()['question_text']
|
327
|
+
'How are you?'
|
328
|
+
"""
|
329
|
+
return getattr(self, key)
|
330
|
+
|
332
331
|
def __repr__(self) -> str:
|
333
|
-
"""Return a string representation of the question. Should be able to be used to reconstruct the question.
|
334
|
-
|
332
|
+
"""Return a string representation of the question. Should be able to be used to reconstruct the question.
|
333
|
+
|
334
|
+
>>> from edsl import QuestionFreeText as Q
|
335
|
+
>>> repr(Q.example())
|
336
|
+
'Question(\\'free_text\\', question_name = \"""how_are_you\""", question_text = \"""How are you?\""")'
|
337
|
+
"""
|
335
338
|
items = [
|
336
339
|
f'{k} = """{v}"""' if isinstance(v, str) else f"{k} = {v}"
|
337
340
|
for k, v in self.data.items()
|
@@ -340,14 +343,31 @@ class QuestionBase(
|
|
340
343
|
question_type = self.to_dict().get("question_type", "None")
|
341
344
|
return f"Question('{question_type}', {', '.join(items)})"
|
342
345
|
|
343
|
-
def __eq__(self, other: Type[QuestionBase]) -> bool:
|
344
|
-
"""Check if two questions are equal. Equality is defined as having the .to_dict().
|
346
|
+
def __eq__(self, other: Union[Any, Type[QuestionBase]]) -> bool:
|
347
|
+
"""Check if two questions are equal. Equality is defined as having the .to_dict().
|
348
|
+
|
349
|
+
>>> from edsl import QuestionFreeText as Q
|
350
|
+
>>> q1 = Q.example()
|
351
|
+
>>> q2 = Q.example()
|
352
|
+
>>> q1 == q2
|
353
|
+
True
|
354
|
+
>>> q1.question_text = "How are you John?"
|
355
|
+
>>> q1 == q2
|
356
|
+
False
|
357
|
+
|
358
|
+
"""
|
345
359
|
if not isinstance(other, QuestionBase):
|
346
360
|
return False
|
347
361
|
return self.to_dict() == other.to_dict()
|
348
362
|
|
349
363
|
def __sub__(self, other) -> BaseDiff:
|
350
|
-
"""Return the difference between two objects.
|
364
|
+
"""Return the difference between two objects.
|
365
|
+
>>> from edsl import QuestionFreeText as Q
|
366
|
+
>>> q1 = Q.example()
|
367
|
+
>>> q2 = q1.copy()
|
368
|
+
>>> q2.question_text = "How are you John?"
|
369
|
+
>>> diff = q1 - q2
|
370
|
+
"""
|
351
371
|
|
352
372
|
return BaseDiff(other, self)
|
353
373
|
|
@@ -364,57 +384,51 @@ class QuestionBase(
|
|
364
384
|
):
|
365
385
|
return other_question_or_diff.apply(self)
|
366
386
|
|
367
|
-
from edsl.questions import compose_questions
|
368
|
-
|
369
|
-
return compose_questions(self, other_question_or_diff)
|
370
|
-
|
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
|
387
|
+
# from edsl.questions import compose_questions
|
388
|
+
# return compose_questions(self, other_question_or_diff)
|
375
389
|
|
376
|
-
def _validate_response(self, response):
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
390
|
+
# def _validate_response(self, response):
|
391
|
+
# """Validate the response from the LLM. Behavior depends on the question type."""
|
392
|
+
# if "answer" not in response:
|
393
|
+
# raise QuestionResponseValidationError(
|
394
|
+
# "Response from LLM does not have an answer"
|
395
|
+
# )
|
396
|
+
# return response
|
383
397
|
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
398
|
+
def _translate_answer_code_to_answer(
|
399
|
+
self, answer, scenario: Optional["Scenario"] = None
|
400
|
+
):
|
401
|
+
"""There is over-ridden by child classes that ask for codes."""
|
402
|
+
return answer
|
388
403
|
|
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
|
404
|
+
# endregion
|
393
405
|
|
394
|
-
|
395
|
-
# Forward methods
|
396
|
-
############################
|
406
|
+
# region: Forward methods
|
397
407
|
def add_question(self, other: QuestionBase) -> "Survey":
|
398
|
-
"""Add a question to this question by turning them into a survey with two questions.
|
408
|
+
"""Add a question to this question by turning them into a survey with two questions.
|
409
|
+
|
410
|
+
>>> from edsl.questions import QuestionFreeText as Q
|
411
|
+
>>> from edsl.questions import QuestionMultipleChoice as QMC
|
412
|
+
>>> s = Q.example().add_question(QMC.example())
|
413
|
+
>>> len(s.questions)
|
414
|
+
2
|
415
|
+
"""
|
399
416
|
from edsl.surveys.Survey import Survey
|
400
417
|
|
401
418
|
s = Survey([self, other])
|
402
419
|
return s
|
403
420
|
|
404
421
|
def to_survey(self) -> "Survey":
|
405
|
-
"""Turn a single question into a survey.
|
422
|
+
"""Turn a single question into a survey.
|
423
|
+
>>> from edsl import QuestionFreeText as Q
|
424
|
+
>>> Q.example().to_survey().questions[0].question_name
|
425
|
+
'how_are_you'
|
426
|
+
"""
|
406
427
|
from edsl.surveys.Survey import Survey
|
407
428
|
|
408
429
|
s = Survey([self])
|
409
430
|
return s
|
410
431
|
|
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
432
|
def by(self, *args) -> "Jobs":
|
419
433
|
"""Turn a single question into a survey and then a Job."""
|
420
434
|
from edsl.surveys.Survey import Survey
|
@@ -422,6 +436,15 @@ class QuestionBase(
|
|
422
436
|
s = Survey([self])
|
423
437
|
return s.by(*args)
|
424
438
|
|
439
|
+
# endregion
|
440
|
+
|
441
|
+
# region: Display methods
|
442
|
+
def print(self):
|
443
|
+
from rich import print_json
|
444
|
+
import json
|
445
|
+
|
446
|
+
print_json(json.dumps(self.to_dict()))
|
447
|
+
|
425
448
|
def human_readable(self) -> str:
|
426
449
|
"""Print the question in a human readable format.
|
427
450
|
|
@@ -442,6 +465,7 @@ class QuestionBase(
|
|
442
465
|
self,
|
443
466
|
scenario: Optional[dict] = None,
|
444
467
|
agent: Optional[dict] = {},
|
468
|
+
answers: Optional[dict] = None,
|
445
469
|
include_question_name: bool = False,
|
446
470
|
height: Optional[int] = None,
|
447
471
|
width: Optional[int] = None,
|
@@ -453,6 +477,17 @@ class QuestionBase(
|
|
453
477
|
if scenario is None:
|
454
478
|
scenario = {}
|
455
479
|
|
480
|
+
prior_answers_dict = {}
|
481
|
+
|
482
|
+
if isinstance(answers, dict):
|
483
|
+
for key, value in answers.items():
|
484
|
+
if not key.endswith("_comment") and not key.endswith(
|
485
|
+
"_generated_tokens"
|
486
|
+
):
|
487
|
+
prior_answers_dict[key] = {"answer": value}
|
488
|
+
|
489
|
+
# breakpoint()
|
490
|
+
|
456
491
|
base_template = """
|
457
492
|
<div id="{{ question_name }}" class="survey_question" data-type="{{ question_type }}">
|
458
493
|
{% if include_question_name %}
|
@@ -472,13 +507,40 @@ class QuestionBase(
|
|
472
507
|
|
473
508
|
base_template = Template(base_template)
|
474
509
|
|
475
|
-
|
476
|
-
"
|
477
|
-
"
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
510
|
+
context = {
|
511
|
+
"scenario": scenario,
|
512
|
+
"agent": agent,
|
513
|
+
} | prior_answers_dict
|
514
|
+
|
515
|
+
# Render the question text
|
516
|
+
try:
|
517
|
+
question_text = Template(self.question_text).render(context)
|
518
|
+
except Exception as e:
|
519
|
+
print(
|
520
|
+
f"Error rendering question: question_text = {self.question_text}, error = {e}"
|
521
|
+
)
|
522
|
+
question_text = self.question_text
|
523
|
+
|
524
|
+
try:
|
525
|
+
question_content = Template(question_content).render(context)
|
526
|
+
except Exception as e:
|
527
|
+
print(
|
528
|
+
f"Error rendering question: question_content = {question_content}, error = {e}"
|
529
|
+
)
|
530
|
+
question_content = question_content
|
531
|
+
|
532
|
+
try:
|
533
|
+
params = {
|
534
|
+
"question_name": self.question_name,
|
535
|
+
"question_text": question_text,
|
536
|
+
"question_type": self.question_type,
|
537
|
+
"question_content": question_content,
|
538
|
+
"include_question_name": include_question_name,
|
539
|
+
}
|
540
|
+
except Exception as e:
|
541
|
+
raise ValueError(
|
542
|
+
f"Error rendering question: params = {params}, error = {e}"
|
543
|
+
)
|
482
544
|
rendered_html = base_template.render(**params)
|
483
545
|
|
484
546
|
if iframe:
|
@@ -497,6 +559,21 @@ class QuestionBase(
|
|
497
559
|
|
498
560
|
return rendered_html
|
499
561
|
|
562
|
+
@classmethod
|
563
|
+
def example_model(cls):
|
564
|
+
from edsl import Model
|
565
|
+
|
566
|
+
q = cls.example()
|
567
|
+
m = Model("test", canned_response=cls._simulate_answer(q)["answer"])
|
568
|
+
|
569
|
+
return m
|
570
|
+
|
571
|
+
@classmethod
|
572
|
+
def example_results(cls):
|
573
|
+
m = cls.example_model()
|
574
|
+
q = cls.example()
|
575
|
+
return q.by(m).run(cache=False)
|
576
|
+
|
500
577
|
def rich_print(self):
|
501
578
|
"""Print the question in a rich format."""
|
502
579
|
from rich.table import Table
|
@@ -520,6 +597,8 @@ class QuestionBase(
|
|
520
597
|
)
|
521
598
|
return table
|
522
599
|
|
600
|
+
# endregion
|
601
|
+
|
523
602
|
|
524
603
|
if __name__ == "__main__":
|
525
604
|
import doctest
|