edsl 0.1.32__py3-none-any.whl → 0.1.33__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- edsl/Base.py +9 -3
- edsl/TemplateLoader.py +24 -0
- edsl/__init__.py +8 -3
- edsl/__version__.py +1 -1
- edsl/agents/Agent.py +40 -8
- edsl/agents/AgentList.py +43 -0
- edsl/agents/Invigilator.py +135 -219
- edsl/agents/InvigilatorBase.py +148 -59
- edsl/agents/{PromptConstructionMixin.py → PromptConstructor.py} +138 -89
- edsl/agents/__init__.py +1 -0
- edsl/auto/AutoStudy.py +117 -0
- edsl/auto/StageBase.py +230 -0
- edsl/auto/StageGenerateSurvey.py +178 -0
- edsl/auto/StageLabelQuestions.py +125 -0
- edsl/auto/StagePersona.py +61 -0
- edsl/auto/StagePersonaDimensionValueRanges.py +88 -0
- edsl/auto/StagePersonaDimensionValues.py +74 -0
- edsl/auto/StagePersonaDimensions.py +69 -0
- edsl/auto/StageQuestions.py +73 -0
- edsl/auto/SurveyCreatorPipeline.py +21 -0
- edsl/auto/utilities.py +224 -0
- edsl/config.py +47 -56
- edsl/coop/PriceFetcher.py +58 -0
- edsl/coop/coop.py +50 -7
- edsl/data/Cache.py +35 -1
- edsl/data_transfer_models.py +73 -38
- edsl/enums.py +4 -0
- edsl/exceptions/language_models.py +25 -1
- edsl/exceptions/questions.py +62 -5
- edsl/exceptions/results.py +4 -0
- edsl/inference_services/AnthropicService.py +13 -11
- edsl/inference_services/AwsBedrock.py +19 -17
- edsl/inference_services/AzureAI.py +37 -20
- edsl/inference_services/GoogleService.py +16 -12
- edsl/inference_services/GroqService.py +2 -0
- edsl/inference_services/InferenceServiceABC.py +58 -3
- edsl/inference_services/MistralAIService.py +120 -0
- edsl/inference_services/OpenAIService.py +48 -54
- edsl/inference_services/TestService.py +80 -0
- edsl/inference_services/TogetherAIService.py +170 -0
- edsl/inference_services/models_available_cache.py +0 -6
- edsl/inference_services/registry.py +6 -0
- edsl/jobs/Answers.py +10 -12
- edsl/jobs/FailedQuestion.py +78 -0
- edsl/jobs/Jobs.py +37 -22
- edsl/jobs/buckets/BucketCollection.py +24 -15
- edsl/jobs/buckets/TokenBucket.py +93 -14
- edsl/jobs/interviews/Interview.py +366 -78
- edsl/jobs/interviews/{interview_exception_tracking.py → InterviewExceptionCollection.py} +14 -68
- edsl/jobs/interviews/InterviewExceptionEntry.py +85 -19
- edsl/jobs/runners/JobsRunnerAsyncio.py +146 -175
- edsl/jobs/runners/JobsRunnerStatus.py +331 -0
- edsl/jobs/tasks/QuestionTaskCreator.py +30 -23
- edsl/jobs/tasks/TaskHistory.py +148 -213
- edsl/language_models/LanguageModel.py +261 -156
- edsl/language_models/ModelList.py +2 -2
- edsl/language_models/RegisterLanguageModelsMeta.py +14 -29
- edsl/language_models/fake_openai_call.py +15 -0
- edsl/language_models/fake_openai_service.py +61 -0
- edsl/language_models/registry.py +23 -6
- edsl/language_models/repair.py +0 -19
- edsl/language_models/utilities.py +61 -0
- edsl/notebooks/Notebook.py +20 -2
- edsl/prompts/Prompt.py +52 -2
- edsl/questions/AnswerValidatorMixin.py +23 -26
- edsl/questions/QuestionBase.py +330 -249
- edsl/questions/QuestionBaseGenMixin.py +133 -0
- edsl/questions/QuestionBasePromptsMixin.py +266 -0
- edsl/questions/QuestionBudget.py +99 -41
- edsl/questions/QuestionCheckBox.py +227 -35
- edsl/questions/QuestionExtract.py +98 -27
- edsl/questions/QuestionFreeText.py +52 -29
- edsl/questions/QuestionFunctional.py +7 -0
- edsl/questions/QuestionList.py +141 -22
- edsl/questions/QuestionMultipleChoice.py +159 -65
- edsl/questions/QuestionNumerical.py +88 -46
- edsl/questions/QuestionRank.py +182 -24
- edsl/questions/Quick.py +41 -0
- edsl/questions/RegisterQuestionsMeta.py +31 -12
- edsl/questions/ResponseValidatorABC.py +170 -0
- edsl/questions/__init__.py +3 -4
- edsl/questions/decorators.py +21 -0
- edsl/questions/derived/QuestionLikertFive.py +10 -5
- edsl/questions/derived/QuestionLinearScale.py +15 -2
- edsl/questions/derived/QuestionTopK.py +10 -1
- edsl/questions/derived/QuestionYesNo.py +24 -3
- edsl/questions/descriptors.py +43 -7
- edsl/questions/prompt_templates/question_budget.jinja +13 -0
- edsl/questions/prompt_templates/question_checkbox.jinja +32 -0
- edsl/questions/prompt_templates/question_extract.jinja +11 -0
- edsl/questions/prompt_templates/question_free_text.jinja +3 -0
- edsl/questions/prompt_templates/question_linear_scale.jinja +11 -0
- edsl/questions/prompt_templates/question_list.jinja +17 -0
- edsl/questions/prompt_templates/question_multiple_choice.jinja +33 -0
- edsl/questions/prompt_templates/question_numerical.jinja +37 -0
- edsl/questions/question_registry.py +6 -2
- edsl/questions/templates/__init__.py +0 -0
- edsl/questions/templates/budget/__init__.py +0 -0
- edsl/questions/templates/budget/answering_instructions.jinja +7 -0
- edsl/questions/templates/budget/question_presentation.jinja +7 -0
- edsl/questions/templates/checkbox/__init__.py +0 -0
- edsl/questions/templates/checkbox/answering_instructions.jinja +10 -0
- edsl/questions/templates/checkbox/question_presentation.jinja +22 -0
- edsl/questions/templates/extract/__init__.py +0 -0
- edsl/questions/templates/extract/answering_instructions.jinja +7 -0
- edsl/questions/templates/extract/question_presentation.jinja +1 -0
- edsl/questions/templates/free_text/__init__.py +0 -0
- edsl/questions/templates/free_text/answering_instructions.jinja +0 -0
- edsl/questions/templates/free_text/question_presentation.jinja +1 -0
- edsl/questions/templates/likert_five/__init__.py +0 -0
- edsl/questions/templates/likert_five/answering_instructions.jinja +10 -0
- edsl/questions/templates/likert_five/question_presentation.jinja +12 -0
- edsl/questions/templates/linear_scale/__init__.py +0 -0
- edsl/questions/templates/linear_scale/answering_instructions.jinja +5 -0
- edsl/questions/templates/linear_scale/question_presentation.jinja +5 -0
- edsl/questions/templates/list/__init__.py +0 -0
- edsl/questions/templates/list/answering_instructions.jinja +4 -0
- edsl/questions/templates/list/question_presentation.jinja +5 -0
- edsl/questions/templates/multiple_choice/__init__.py +0 -0
- edsl/questions/templates/multiple_choice/answering_instructions.jinja +9 -0
- edsl/questions/templates/multiple_choice/html.jinja +0 -0
- edsl/questions/templates/multiple_choice/question_presentation.jinja +12 -0
- edsl/questions/templates/numerical/__init__.py +0 -0
- edsl/questions/templates/numerical/answering_instructions.jinja +8 -0
- edsl/questions/templates/numerical/question_presentation.jinja +7 -0
- edsl/questions/templates/rank/__init__.py +0 -0
- edsl/questions/templates/rank/answering_instructions.jinja +11 -0
- edsl/questions/templates/rank/question_presentation.jinja +15 -0
- edsl/questions/templates/top_k/__init__.py +0 -0
- edsl/questions/templates/top_k/answering_instructions.jinja +8 -0
- edsl/questions/templates/top_k/question_presentation.jinja +22 -0
- edsl/questions/templates/yes_no/__init__.py +0 -0
- edsl/questions/templates/yes_no/answering_instructions.jinja +6 -0
- edsl/questions/templates/yes_no/question_presentation.jinja +12 -0
- edsl/results/Dataset.py +20 -0
- edsl/results/DatasetExportMixin.py +46 -48
- edsl/results/DatasetTree.py +145 -0
- edsl/results/Result.py +32 -5
- edsl/results/Results.py +135 -46
- edsl/results/ResultsDBMixin.py +3 -3
- edsl/results/Selector.py +118 -0
- edsl/results/tree_explore.py +115 -0
- edsl/scenarios/FileStore.py +71 -10
- edsl/scenarios/Scenario.py +96 -25
- edsl/scenarios/ScenarioImageMixin.py +2 -2
- edsl/scenarios/ScenarioList.py +361 -39
- edsl/scenarios/ScenarioListExportMixin.py +9 -0
- edsl/scenarios/ScenarioListPdfMixin.py +150 -4
- edsl/study/SnapShot.py +8 -1
- edsl/study/Study.py +32 -0
- edsl/surveys/Rule.py +10 -1
- edsl/surveys/RuleCollection.py +21 -5
- edsl/surveys/Survey.py +637 -311
- edsl/surveys/SurveyExportMixin.py +71 -9
- edsl/surveys/SurveyFlowVisualizationMixin.py +2 -1
- edsl/surveys/SurveyQualtricsImport.py +75 -4
- edsl/surveys/instructions/ChangeInstruction.py +47 -0
- edsl/surveys/instructions/Instruction.py +34 -0
- edsl/surveys/instructions/InstructionCollection.py +77 -0
- edsl/surveys/instructions/__init__.py +0 -0
- edsl/templates/error_reporting/base.html +24 -0
- edsl/templates/error_reporting/exceptions_by_model.html +35 -0
- edsl/templates/error_reporting/exceptions_by_question_name.html +17 -0
- edsl/templates/error_reporting/exceptions_by_type.html +17 -0
- edsl/templates/error_reporting/interview_details.html +116 -0
- edsl/templates/error_reporting/interviews.html +10 -0
- edsl/templates/error_reporting/overview.html +5 -0
- edsl/templates/error_reporting/performance_plot.html +2 -0
- edsl/templates/error_reporting/report.css +74 -0
- edsl/templates/error_reporting/report.html +118 -0
- edsl/templates/error_reporting/report.js +25 -0
- edsl/utilities/utilities.py +9 -1
- {edsl-0.1.32.dist-info → edsl-0.1.33.dist-info}/METADATA +5 -2
- edsl-0.1.33.dist-info/RECORD +295 -0
- edsl/jobs/interviews/InterviewTaskBuildingMixin.py +0 -286
- edsl/jobs/interviews/retry_management.py +0 -37
- edsl/jobs/runners/JobsRunnerStatusMixin.py +0 -333
- edsl/utilities/gcp_bucket/simple_example.py +0 -9
- edsl-0.1.32.dist-info/RECORD +0 -209
- {edsl-0.1.32.dist-info → edsl-0.1.33.dist-info}/LICENSE +0 -0
- {edsl-0.1.32.dist-info → edsl-0.1.33.dist-info}/WHEEL +0 -0
@@ -0,0 +1,133 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
import copy
|
3
|
+
import itertools
|
4
|
+
from typing import Optional, List, Callable, Type
|
5
|
+
from typing import TypeVar
|
6
|
+
|
7
|
+
|
8
|
+
class QuestionBaseGenMixin:
|
9
|
+
def copy(self) -> QuestionBase:
|
10
|
+
"""Return a deep copy of the question.
|
11
|
+
|
12
|
+
>>> from edsl.questions import QuestionFreeText
|
13
|
+
>>> q = QuestionFreeText(question_name = "color", question_text = "What is your favorite color?")
|
14
|
+
>>> q2 = q.copy()
|
15
|
+
>>> q2.question_name
|
16
|
+
'color'
|
17
|
+
|
18
|
+
"""
|
19
|
+
return copy.deepcopy(self)
|
20
|
+
|
21
|
+
def option_permutations(self) -> list[QuestionBase]:
|
22
|
+
"""Return a list of questions with all possible permutations of the options.
|
23
|
+
|
24
|
+
>>> from edsl import QuestionMultipleChoice as Q
|
25
|
+
>>> len(Q.example().option_permutations())
|
26
|
+
24
|
27
|
+
"""
|
28
|
+
|
29
|
+
if not hasattr(self, "question_options"):
|
30
|
+
return [self]
|
31
|
+
|
32
|
+
questions = []
|
33
|
+
for index, permutation in enumerate(
|
34
|
+
itertools.permutations(self.question_options)
|
35
|
+
):
|
36
|
+
question = copy.deepcopy(self)
|
37
|
+
question.question_options = list(permutation)
|
38
|
+
question.question_name = f"{self.question_name}_{index}"
|
39
|
+
questions.append(question)
|
40
|
+
return questions
|
41
|
+
|
42
|
+
def loop(self, scenario_list: ScenarioList) -> List[QuestionBase]:
|
43
|
+
"""Return a list of questions with the question name modified for each scenario.
|
44
|
+
|
45
|
+
:param scenario_list: The list of scenarios to loop through.
|
46
|
+
|
47
|
+
>>> from edsl import QuestionFreeText
|
48
|
+
>>> from edsl import ScenarioList
|
49
|
+
>>> q = QuestionFreeText(question_text = "What are your thoughts on: {{ subject}}?", question_name = "base_{{subject}}")
|
50
|
+
>>> len(q.loop(ScenarioList.from_list("subject", ["Math", "Economics", "Chemistry"])))
|
51
|
+
3
|
52
|
+
|
53
|
+
"""
|
54
|
+
from jinja2 import Environment
|
55
|
+
from edsl.questions.QuestionBase import QuestionBase
|
56
|
+
|
57
|
+
starting_name = self.question_name
|
58
|
+
questions = []
|
59
|
+
for index, scenario in enumerate(scenario_list):
|
60
|
+
env = Environment()
|
61
|
+
new_data = self.to_dict().copy()
|
62
|
+
for key, value in [(k, v) for k, v in new_data.items() if v is not None]:
|
63
|
+
if (
|
64
|
+
isinstance(value, str) or isinstance(value, int)
|
65
|
+
) and key != "question_options":
|
66
|
+
new_data[key] = env.from_string(value).render(scenario)
|
67
|
+
elif isinstance(value, list):
|
68
|
+
new_data[key] = [
|
69
|
+
env.from_string(v).render(scenario) if isinstance(v, str) else v
|
70
|
+
for v in value
|
71
|
+
]
|
72
|
+
elif isinstance(value, dict):
|
73
|
+
new_data[key] = {
|
74
|
+
(
|
75
|
+
env.from_string(k).render(scenario)
|
76
|
+
if isinstance(k, str)
|
77
|
+
else k
|
78
|
+
): (
|
79
|
+
env.from_string(v).render(scenario)
|
80
|
+
if isinstance(v, str)
|
81
|
+
else v
|
82
|
+
)
|
83
|
+
for k, v in value.items()
|
84
|
+
}
|
85
|
+
elif key == "question_options" and isinstance(value, str):
|
86
|
+
new_data[key] = value
|
87
|
+
else:
|
88
|
+
raise ValueError(
|
89
|
+
f"Unexpected value type: {type(value)} for key '{key}'"
|
90
|
+
)
|
91
|
+
|
92
|
+
if new_data["question_name"] == starting_name:
|
93
|
+
new_data["question_name"] = new_data["question_name"] + f"_{index}"
|
94
|
+
|
95
|
+
questions.append(QuestionBase.from_dict(new_data))
|
96
|
+
return questions
|
97
|
+
|
98
|
+
def apply_function(self, func: Callable, exclude_components=None) -> QuestionBase:
|
99
|
+
"""Apply a function to the question parts
|
100
|
+
|
101
|
+
>>> from edsl.questions import QuestionFreeText
|
102
|
+
>>> q = QuestionFreeText(question_name = "color", question_text = "What is your favorite color?")
|
103
|
+
>>> shouting = lambda x: x.upper()
|
104
|
+
>>> q.apply_function(shouting)
|
105
|
+
Question('free_text', question_name = \"""color\""", question_text = \"""WHAT IS YOUR FAVORITE COLOR?\""")
|
106
|
+
|
107
|
+
"""
|
108
|
+
from edsl.questions.QuestionBase import QuestionBase
|
109
|
+
|
110
|
+
if exclude_components is None:
|
111
|
+
exclude_components = ["question_name", "question_type"]
|
112
|
+
|
113
|
+
d = copy.deepcopy(self._to_dict())
|
114
|
+
for key, value in d.items():
|
115
|
+
if key in exclude_components:
|
116
|
+
continue
|
117
|
+
if isinstance(value, dict):
|
118
|
+
for k, v in value.items():
|
119
|
+
value[k] = func(v)
|
120
|
+
d[key] = value
|
121
|
+
continue
|
122
|
+
if isinstance(value, list):
|
123
|
+
value = [func(v) for v in value]
|
124
|
+
d[key] = value
|
125
|
+
continue
|
126
|
+
d[key] = func(value)
|
127
|
+
return QuestionBase.from_dict(d)
|
128
|
+
|
129
|
+
|
130
|
+
if __name__ == "__main__":
|
131
|
+
import doctest
|
132
|
+
|
133
|
+
doctest.testmod()
|
@@ -0,0 +1,266 @@
|
|
1
|
+
from importlib import resources
|
2
|
+
from typing import Optional
|
3
|
+
from edsl.prompts import Prompt
|
4
|
+
from edsl.exceptions.questions import QuestionAnswerValidationError
|
5
|
+
|
6
|
+
from functools import lru_cache
|
7
|
+
|
8
|
+
|
9
|
+
class TemplateManager:
|
10
|
+
_instance = None
|
11
|
+
|
12
|
+
def __new__(cls):
|
13
|
+
if cls._instance is None:
|
14
|
+
cls._instance = super().__new__(cls)
|
15
|
+
cls._instance._template_cache = {}
|
16
|
+
return cls._instance
|
17
|
+
|
18
|
+
@lru_cache(maxsize=None)
|
19
|
+
def get_template(self, question_type, template_name):
|
20
|
+
if (question_type, template_name) not in self._template_cache:
|
21
|
+
with resources.open_text(
|
22
|
+
f"edsl.questions.templates.{question_type}", template_name
|
23
|
+
) as file:
|
24
|
+
self._template_cache[(question_type, template_name)] = file.read()
|
25
|
+
return self._template_cache[(question_type, template_name)]
|
26
|
+
|
27
|
+
|
28
|
+
# Global instance
|
29
|
+
template_manager = TemplateManager()
|
30
|
+
|
31
|
+
|
32
|
+
class QuestionBasePromptsMixin:
|
33
|
+
# @classmethod
|
34
|
+
# @lru_cache(maxsize=1)
|
35
|
+
# def _read_template(cls, template_name):
|
36
|
+
# with resources.open_text(
|
37
|
+
# f"edsl.questions.templates.{cls.question_type}", template_name
|
38
|
+
# ) as file:
|
39
|
+
# return file.read()
|
40
|
+
|
41
|
+
@classmethod
|
42
|
+
def applicable_prompts(
|
43
|
+
cls, model: Optional[str] = None
|
44
|
+
) -> list[type["PromptBase"]]:
|
45
|
+
"""Get the prompts that are applicable to the question type.
|
46
|
+
|
47
|
+
:param model: The language model to use.
|
48
|
+
|
49
|
+
>>> from edsl.questions import QuestionFreeText
|
50
|
+
>>> QuestionFreeText.applicable_prompts()
|
51
|
+
[<class 'edsl.prompts.library.question_freetext.FreeText'>]
|
52
|
+
|
53
|
+
:param model: The language model to use. If None, assumes does not matter.
|
54
|
+
|
55
|
+
"""
|
56
|
+
from edsl.prompts.registry import get_classes as prompt_lookup
|
57
|
+
|
58
|
+
applicable_prompts = prompt_lookup(
|
59
|
+
component_type="question_instructions",
|
60
|
+
question_type=cls.question_type,
|
61
|
+
model=model,
|
62
|
+
)
|
63
|
+
return applicable_prompts
|
64
|
+
|
65
|
+
@property
|
66
|
+
def model_instructions(self) -> dict:
|
67
|
+
"""Get the model-specific instructions for the question."""
|
68
|
+
if not hasattr(self, "_model_instructions"):
|
69
|
+
self._model_instructions = {}
|
70
|
+
return self._model_instructions
|
71
|
+
|
72
|
+
def _all_text(self) -> str:
|
73
|
+
"""Return the question text.
|
74
|
+
|
75
|
+
>>> from edsl import QuestionMultipleChoice as Q
|
76
|
+
>>> Q.example()._all_text()
|
77
|
+
"how_feelingHow are you?['Good', 'Great', 'OK', 'Bad']"
|
78
|
+
"""
|
79
|
+
txt = ""
|
80
|
+
for key, value in self.data.items():
|
81
|
+
if isinstance(value, str):
|
82
|
+
txt += value
|
83
|
+
elif isinstance(value, list):
|
84
|
+
txt += "".join(str(value))
|
85
|
+
return txt
|
86
|
+
|
87
|
+
@model_instructions.setter
|
88
|
+
def model_instructions(self, data: dict):
|
89
|
+
"""Set the model-specific instructions for the question."""
|
90
|
+
self._model_instructions = data
|
91
|
+
|
92
|
+
def add_model_instructions(
|
93
|
+
self, *, instructions: str, model: Optional[str] = None
|
94
|
+
) -> None:
|
95
|
+
"""Add model-specific instructions for the question that override the default instructions.
|
96
|
+
|
97
|
+
:param instructions: The instructions to add. This is typically a jinja2 template.
|
98
|
+
:param model: The language model for this instruction.
|
99
|
+
|
100
|
+
>>> from edsl.questions import QuestionFreeText
|
101
|
+
>>> q = QuestionFreeText(question_name = "color", question_text = "What is your favorite color?")
|
102
|
+
>>> q.add_model_instructions(instructions = "{{question_text}}. Answer in valid JSON like so {'answer': 'comment: <>}", model = "gpt3")
|
103
|
+
>>> q.get_instructions(model = "gpt3")
|
104
|
+
Prompt(text=\"""{{question_text}}. Answer in valid JSON like so {'answer': 'comment: <>}\""")
|
105
|
+
"""
|
106
|
+
from edsl import Model
|
107
|
+
|
108
|
+
if not hasattr(self, "_model_instructions"):
|
109
|
+
self._model_instructions = {}
|
110
|
+
if model is None:
|
111
|
+
# if not model is passed, all the models are mapped to this instruction, including 'None'
|
112
|
+
self._model_instructions = {
|
113
|
+
model_name: instructions
|
114
|
+
for model_name in Model.available(name_only=True)
|
115
|
+
}
|
116
|
+
self._model_instructions.update({model: instructions})
|
117
|
+
else:
|
118
|
+
self._model_instructions.update({model: instructions})
|
119
|
+
|
120
|
+
@classmethod
|
121
|
+
def path_to_folder(cls) -> str:
|
122
|
+
return resources.files(f"edsl.questions.templates", cls.question_type)
|
123
|
+
|
124
|
+
@property
|
125
|
+
def response_model(self) -> type["BaseModel"]:
|
126
|
+
if self._response_model is not None:
|
127
|
+
return self._response_model
|
128
|
+
else:
|
129
|
+
return self.create_response_model()
|
130
|
+
|
131
|
+
@property
|
132
|
+
def use_code(self) -> bool:
|
133
|
+
if hasattr(self, "_use_code"):
|
134
|
+
return self._use_code
|
135
|
+
return True
|
136
|
+
|
137
|
+
@use_code.setter
|
138
|
+
def use_code(self, value: bool) -> None:
|
139
|
+
self._use_code = value
|
140
|
+
|
141
|
+
@property
|
142
|
+
def include_comment(self) -> bool:
|
143
|
+
if hasattr(self, "_include_comment"):
|
144
|
+
return self._include_comment
|
145
|
+
return True
|
146
|
+
|
147
|
+
@include_comment.setter
|
148
|
+
def include_comment(self, value: bool) -> None:
|
149
|
+
self._include_comment = value
|
150
|
+
|
151
|
+
@classmethod
|
152
|
+
def default_answering_instructions(cls) -> str:
|
153
|
+
# template_text = cls._read_template("answering_instructions.jinja")
|
154
|
+
template_text = template_manager.get_template(
|
155
|
+
cls.question_type, "answering_instructions.jinja"
|
156
|
+
)
|
157
|
+
return Prompt(text=template_text)
|
158
|
+
|
159
|
+
@classmethod
|
160
|
+
def default_question_presentation(cls):
|
161
|
+
# template_text = cls._read_template("question_presentation.jinja")
|
162
|
+
template_text = template_manager.get_template(
|
163
|
+
cls.question_type, "question_presentation.jinja"
|
164
|
+
)
|
165
|
+
return Prompt(text=template_text)
|
166
|
+
|
167
|
+
@property
|
168
|
+
def answering_instructions(self) -> str:
|
169
|
+
if self._answering_instructions is None:
|
170
|
+
return self.default_answering_instructions()
|
171
|
+
return self._answering_instructions
|
172
|
+
|
173
|
+
@answering_instructions.setter
|
174
|
+
def answering_instructions(self, value) -> None:
|
175
|
+
self._answering_instructions = value
|
176
|
+
|
177
|
+
# @classmethod
|
178
|
+
# def default_answering_instructions(cls) -> str:
|
179
|
+
# with resources.open_text(
|
180
|
+
# f"edsl.questions.templates.{cls.question_type}",
|
181
|
+
# "answering_instructions.jinja",
|
182
|
+
# ) as file:
|
183
|
+
# return Prompt(text=file.read())
|
184
|
+
|
185
|
+
# @classmethod
|
186
|
+
# def default_question_presentation(cls):
|
187
|
+
# with resources.open_text(
|
188
|
+
# f"edsl.questions.templates.{cls.question_type}",
|
189
|
+
# "question_presentation.jinja",
|
190
|
+
# ) as file:
|
191
|
+
# return Prompt(text=file.read())
|
192
|
+
|
193
|
+
@property
|
194
|
+
def question_presentation(self):
|
195
|
+
if self._question_presentation is None:
|
196
|
+
return self.default_question_presentation()
|
197
|
+
return self._question_presentation
|
198
|
+
|
199
|
+
@question_presentation.setter
|
200
|
+
def question_presentation(self, value):
|
201
|
+
self._question_presentation = value
|
202
|
+
|
203
|
+
def prompt_preview(self, scenario=None, agent=None):
|
204
|
+
return self.new_default_instructions.render(
|
205
|
+
self.data
|
206
|
+
| {
|
207
|
+
"include_comment": getattr(self, "_include_comment", True),
|
208
|
+
"use_code": getattr(self, "_use_code", True),
|
209
|
+
}
|
210
|
+
| ({"scenario": scenario} or {})
|
211
|
+
| ({"agent": agent} or {})
|
212
|
+
)
|
213
|
+
|
214
|
+
@classmethod
|
215
|
+
def self_check(cls):
|
216
|
+
q = cls.example()
|
217
|
+
for answer, params in q.response_validator.valid_examples:
|
218
|
+
for key, value in params.items():
|
219
|
+
setattr(q, key, value)
|
220
|
+
q._validate_answer(answer)
|
221
|
+
for answer, params, reason in q.response_validator.invalid_examples:
|
222
|
+
for key, value in params.items():
|
223
|
+
setattr(q, key, value)
|
224
|
+
try:
|
225
|
+
q._validate_answer(answer)
|
226
|
+
except QuestionAnswerValidationError:
|
227
|
+
pass
|
228
|
+
else:
|
229
|
+
raise ValueError(f"Example {answer} should have failed for {reason}.")
|
230
|
+
|
231
|
+
@property
|
232
|
+
def new_default_instructions(self) -> "Prompt":
|
233
|
+
"This is set up as a property because there are mutable question values that determine how it is rendered."
|
234
|
+
return self.question_presentation + self.answering_instructions
|
235
|
+
|
236
|
+
@property
|
237
|
+
def parameters(self) -> set[str]:
|
238
|
+
"""Return the parameters of the question."""
|
239
|
+
from jinja2 import Environment, meta
|
240
|
+
|
241
|
+
env = Environment()
|
242
|
+
# Parse the template
|
243
|
+
txt = self._all_text()
|
244
|
+
# txt = self.question_text
|
245
|
+
# if hasattr(self, "question_options"):
|
246
|
+
# txt += " ".join(self.question_options)
|
247
|
+
parsed_content = env.parse(txt)
|
248
|
+
# Extract undeclared variables
|
249
|
+
variables = meta.find_undeclared_variables(parsed_content)
|
250
|
+
# Return as a list
|
251
|
+
return set(variables)
|
252
|
+
|
253
|
+
def get_instructions(self, model: Optional[str] = None) -> type["PromptBase"]:
|
254
|
+
"""Get the mathcing question-answering instructions for the question.
|
255
|
+
|
256
|
+
:param model: The language model to use.
|
257
|
+
"""
|
258
|
+
from edsl.prompts.Prompt import Prompt
|
259
|
+
|
260
|
+
if model in self.model_instructions:
|
261
|
+
return Prompt(text=self.model_instructions[model])
|
262
|
+
else:
|
263
|
+
if hasattr(self, "new_default_instructions"):
|
264
|
+
return self.new_default_instructions
|
265
|
+
else:
|
266
|
+
return self.applicable_prompts(model)[0]()
|
edsl/questions/QuestionBudget.py
CHANGED
@@ -1,8 +1,60 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
-
import
|
3
|
-
|
2
|
+
from typing import Any, Optional, Union, List
|
3
|
+
|
4
|
+
from pydantic import Field, BaseModel, validator
|
5
|
+
|
4
6
|
from edsl.questions.QuestionBase import QuestionBase
|
5
7
|
from edsl.questions.descriptors import IntegerDescriptor, QuestionOptionsDescriptor
|
8
|
+
from edsl.questions.ResponseValidatorABC import ResponseValidatorABC
|
9
|
+
|
10
|
+
|
11
|
+
class BudgewResponseValidator(ResponseValidatorABC):
|
12
|
+
valid_examples = []
|
13
|
+
|
14
|
+
invalid_examples = []
|
15
|
+
|
16
|
+
def fix(self, response, verbose=False):
|
17
|
+
if verbose:
|
18
|
+
print(f"Fixing list response: {response}")
|
19
|
+
answer = str(response.get("answer") or response.get("generated_tokens", ""))
|
20
|
+
if len(answer.split(",")) > 0:
|
21
|
+
return (
|
22
|
+
{"answer": answer.split(",")} | {"comment": response.get("comment")}
|
23
|
+
if "comment" in response
|
24
|
+
else {}
|
25
|
+
)
|
26
|
+
|
27
|
+
|
28
|
+
def create_budget_model(
|
29
|
+
budget_sum: float, permissive: bool, question_options: List[str]
|
30
|
+
):
|
31
|
+
class BudgetResponse(BaseModel):
|
32
|
+
answer: List[float] = Field(
|
33
|
+
...,
|
34
|
+
description="List of non-negative numbers representing budget allocation",
|
35
|
+
min_items=len(question_options),
|
36
|
+
max_items=len(question_options),
|
37
|
+
)
|
38
|
+
comment: Optional[str] = None
|
39
|
+
generated_tokens: Optional[str] = None
|
40
|
+
|
41
|
+
@validator("answer")
|
42
|
+
def validate_answer(cls, v):
|
43
|
+
if len(v) != len(question_options):
|
44
|
+
raise ValueError(f"Must provide {len(question_options)} values")
|
45
|
+
if any(x < 0 for x in v):
|
46
|
+
raise ValueError("All values must be non-negative")
|
47
|
+
total = sum(v)
|
48
|
+
if not permissive and total != budget_sum:
|
49
|
+
raise ValueError(f"Sum of numbers must equal {budget_sum}")
|
50
|
+
elif permissive and total > budget_sum:
|
51
|
+
raise ValueError(f"Sum of numbers cannot exceed {budget_sum}")
|
52
|
+
return v
|
53
|
+
|
54
|
+
class Config:
|
55
|
+
extra = "forbid"
|
56
|
+
|
57
|
+
return BudgetResponse
|
6
58
|
|
7
59
|
|
8
60
|
class QuestionBudget(QuestionBase):
|
@@ -11,6 +63,8 @@ class QuestionBudget(QuestionBase):
|
|
11
63
|
question_type = "budget"
|
12
64
|
budget_sum: int = IntegerDescriptor(none_allowed=False)
|
13
65
|
question_options: list[str] = QuestionOptionsDescriptor(q_budget=True)
|
66
|
+
_response_model = None
|
67
|
+
response_validator_class = BudgewResponseValidator
|
14
68
|
|
15
69
|
def __init__(
|
16
70
|
self,
|
@@ -18,6 +72,10 @@ class QuestionBudget(QuestionBase):
|
|
18
72
|
question_text: str,
|
19
73
|
question_options: list[str],
|
20
74
|
budget_sum: int,
|
75
|
+
include_comment: bool = True,
|
76
|
+
question_presentation: Optional[str] = None,
|
77
|
+
answering_instructions: Optional[str] = None,
|
78
|
+
permissive: bool = False,
|
21
79
|
):
|
22
80
|
"""Instantiate a new QuestionBudget.
|
23
81
|
|
@@ -30,20 +88,19 @@ class QuestionBudget(QuestionBase):
|
|
30
88
|
self.question_text = question_text
|
31
89
|
self.question_options = question_options
|
32
90
|
self.budget_sum = budget_sum
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
return answer
|
91
|
+
self.question_presentation = question_presentation
|
92
|
+
self.answering_instructions = answering_instructions
|
93
|
+
self.permissive = permissive
|
94
|
+
self.include_comment = include_comment
|
95
|
+
|
96
|
+
def create_response_model(self):
|
97
|
+
return create_budget_model(
|
98
|
+
self.budget_sum, self.permissive, self.question_options
|
99
|
+
)
|
43
100
|
|
44
101
|
def _translate_answer_code_to_answer(
|
45
|
-
self,
|
46
|
-
):
|
102
|
+
self, answer_code, combined_dict
|
103
|
+
) -> list[dict]:
|
47
104
|
"""
|
48
105
|
Translate the answer codes to the actual answers.
|
49
106
|
|
@@ -52,35 +109,35 @@ class QuestionBudget(QuestionBase):
|
|
52
109
|
This code will translate that to "a".
|
53
110
|
"""
|
54
111
|
translated_codes = []
|
55
|
-
for answer_code,
|
56
|
-
translated_codes.append({
|
112
|
+
for answer_code, question_option in zip(answer_code, self.question_options):
|
113
|
+
translated_codes.append({question_option: answer_code})
|
57
114
|
|
58
115
|
return translated_codes
|
59
116
|
|
60
|
-
def _simulate_answer(self, human_readable=True):
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
117
|
+
# def _simulate_answer(self, human_readable=True):
|
118
|
+
# """Simulate a valid answer for debugging purposes (what the validator expects)."""
|
119
|
+
# from edsl.utilities.utilities import random_string
|
120
|
+
|
121
|
+
# if human_readable:
|
122
|
+
# keys = self.question_options
|
123
|
+
# else:
|
124
|
+
# keys = range(len(self.question_options))
|
125
|
+
# remaining_budget = self.budget_sum
|
126
|
+
# values = []
|
127
|
+
# for _ in range(len(self.question_options)):
|
128
|
+
# if _ == len(self.question_options) - 1:
|
129
|
+
# # Assign remaining budget to the last value
|
130
|
+
# values.append(remaining_budget)
|
131
|
+
# else:
|
132
|
+
# # Generate a random value between 0 and remaining budget
|
133
|
+
# value = random.randint(0, remaining_budget)
|
134
|
+
# values.append(value)
|
135
|
+
# remaining_budget -= value
|
136
|
+
# answer = dict(zip(keys, values))
|
137
|
+
# return {
|
138
|
+
# "answer": answer,
|
139
|
+
# "comment": random_string(),
|
140
|
+
# }
|
84
141
|
|
85
142
|
@property
|
86
143
|
def question_html_content(self) -> str:
|
@@ -127,13 +184,14 @@ class QuestionBudget(QuestionBase):
|
|
127
184
|
# Helpful methods
|
128
185
|
################
|
129
186
|
@classmethod
|
130
|
-
def example(cls) -> QuestionBudget:
|
187
|
+
def example(cls, include_comment: bool = True) -> QuestionBudget:
|
131
188
|
"""Return an example of a budget question."""
|
132
189
|
return cls(
|
133
190
|
question_name="food_budget",
|
134
191
|
question_text="How would you allocate $100?",
|
135
192
|
question_options=["Pizza", "Ice Cream", "Burgers", "Salad"],
|
136
193
|
budget_sum=100,
|
194
|
+
include_comment=include_comment,
|
137
195
|
)
|
138
196
|
|
139
197
|
|