edsl 0.1.38.dev4__py3-none-any.whl → 0.1.39__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 +197 -116
- edsl/__init__.py +15 -7
- edsl/__version__.py +1 -1
- edsl/agents/Agent.py +351 -147
- edsl/agents/AgentList.py +211 -73
- edsl/agents/Invigilator.py +101 -50
- edsl/agents/InvigilatorBase.py +62 -70
- edsl/agents/PromptConstructor.py +143 -225
- edsl/agents/QuestionInstructionPromptBuilder.py +128 -0
- edsl/agents/QuestionTemplateReplacementsBuilder.py +137 -0
- edsl/agents/__init__.py +0 -1
- edsl/agents/prompt_helpers.py +3 -3
- edsl/agents/question_option_processor.py +172 -0
- edsl/auto/AutoStudy.py +18 -5
- edsl/auto/StageBase.py +53 -40
- edsl/auto/StageQuestions.py +2 -1
- edsl/auto/utilities.py +0 -6
- edsl/config.py +22 -2
- edsl/conversation/car_buying.py +2 -1
- edsl/coop/CoopFunctionsMixin.py +15 -0
- edsl/coop/ExpectedParrotKeyHandler.py +125 -0
- edsl/coop/PriceFetcher.py +1 -1
- edsl/coop/coop.py +125 -47
- edsl/coop/utils.py +14 -14
- edsl/data/Cache.py +45 -27
- edsl/data/CacheEntry.py +12 -15
- edsl/data/CacheHandler.py +31 -12
- edsl/data/RemoteCacheSync.py +154 -46
- edsl/data/__init__.py +4 -3
- edsl/data_transfer_models.py +2 -1
- edsl/enums.py +27 -0
- edsl/exceptions/__init__.py +50 -50
- edsl/exceptions/agents.py +12 -0
- edsl/exceptions/inference_services.py +5 -0
- edsl/exceptions/questions.py +24 -6
- edsl/exceptions/scenarios.py +7 -0
- edsl/inference_services/AnthropicService.py +38 -19
- edsl/inference_services/AvailableModelCacheHandler.py +184 -0
- edsl/inference_services/AvailableModelFetcher.py +215 -0
- edsl/inference_services/AwsBedrock.py +0 -2
- edsl/inference_services/AzureAI.py +0 -2
- edsl/inference_services/GoogleService.py +7 -12
- edsl/inference_services/InferenceServiceABC.py +18 -85
- edsl/inference_services/InferenceServicesCollection.py +120 -79
- edsl/inference_services/MistralAIService.py +0 -3
- edsl/inference_services/OpenAIService.py +47 -35
- edsl/inference_services/PerplexityService.py +0 -3
- edsl/inference_services/ServiceAvailability.py +135 -0
- edsl/inference_services/TestService.py +11 -10
- edsl/inference_services/TogetherAIService.py +5 -3
- edsl/inference_services/data_structures.py +134 -0
- edsl/jobs/AnswerQuestionFunctionConstructor.py +223 -0
- edsl/jobs/Answers.py +1 -14
- edsl/jobs/FetchInvigilator.py +47 -0
- edsl/jobs/InterviewTaskManager.py +98 -0
- edsl/jobs/InterviewsConstructor.py +50 -0
- edsl/jobs/Jobs.py +356 -431
- edsl/jobs/JobsChecks.py +35 -10
- edsl/jobs/JobsComponentConstructor.py +189 -0
- edsl/jobs/JobsPrompts.py +6 -4
- edsl/jobs/JobsRemoteInferenceHandler.py +205 -133
- edsl/jobs/JobsRemoteInferenceLogger.py +239 -0
- edsl/jobs/RequestTokenEstimator.py +30 -0
- edsl/jobs/async_interview_runner.py +138 -0
- edsl/jobs/buckets/BucketCollection.py +44 -3
- edsl/jobs/buckets/TokenBucket.py +53 -21
- edsl/jobs/buckets/TokenBucketAPI.py +211 -0
- edsl/jobs/buckets/TokenBucketClient.py +191 -0
- edsl/jobs/check_survey_scenario_compatibility.py +85 -0
- edsl/jobs/data_structures.py +120 -0
- edsl/jobs/decorators.py +35 -0
- edsl/jobs/interviews/Interview.py +143 -408
- edsl/jobs/jobs_status_enums.py +9 -0
- edsl/jobs/loggers/HTMLTableJobLogger.py +304 -0
- edsl/jobs/results_exceptions_handler.py +98 -0
- edsl/jobs/runners/JobsRunnerAsyncio.py +88 -403
- edsl/jobs/runners/JobsRunnerStatus.py +133 -165
- edsl/jobs/tasks/QuestionTaskCreator.py +21 -19
- edsl/jobs/tasks/TaskHistory.py +38 -18
- edsl/jobs/tasks/task_status_enum.py +0 -2
- edsl/language_models/ComputeCost.py +63 -0
- edsl/language_models/LanguageModel.py +194 -236
- edsl/language_models/ModelList.py +28 -19
- edsl/language_models/PriceManager.py +127 -0
- edsl/language_models/RawResponseHandler.py +106 -0
- edsl/language_models/ServiceDataSources.py +0 -0
- edsl/language_models/__init__.py +1 -2
- edsl/language_models/key_management/KeyLookup.py +63 -0
- edsl/language_models/key_management/KeyLookupBuilder.py +273 -0
- edsl/language_models/key_management/KeyLookupCollection.py +38 -0
- edsl/language_models/key_management/__init__.py +0 -0
- edsl/language_models/key_management/models.py +131 -0
- edsl/language_models/model.py +256 -0
- edsl/language_models/repair.py +2 -2
- edsl/language_models/utilities.py +5 -4
- edsl/notebooks/Notebook.py +19 -14
- edsl/notebooks/NotebookToLaTeX.py +142 -0
- edsl/prompts/Prompt.py +29 -39
- edsl/questions/ExceptionExplainer.py +77 -0
- edsl/questions/HTMLQuestion.py +103 -0
- edsl/questions/QuestionBase.py +68 -214
- edsl/questions/QuestionBasePromptsMixin.py +7 -3
- edsl/questions/QuestionBudget.py +1 -1
- edsl/questions/QuestionCheckBox.py +3 -3
- edsl/questions/QuestionExtract.py +5 -7
- edsl/questions/QuestionFreeText.py +2 -3
- edsl/questions/QuestionList.py +10 -18
- edsl/questions/QuestionMatrix.py +265 -0
- edsl/questions/QuestionMultipleChoice.py +67 -23
- edsl/questions/QuestionNumerical.py +2 -4
- edsl/questions/QuestionRank.py +7 -17
- edsl/questions/SimpleAskMixin.py +4 -3
- edsl/questions/__init__.py +2 -1
- edsl/questions/{AnswerValidatorMixin.py → answer_validator_mixin.py} +47 -2
- edsl/questions/data_structures.py +20 -0
- edsl/questions/derived/QuestionLinearScale.py +6 -3
- edsl/questions/derived/QuestionTopK.py +1 -1
- edsl/questions/descriptors.py +17 -3
- edsl/questions/loop_processor.py +149 -0
- edsl/questions/{QuestionBaseGenMixin.py → question_base_gen_mixin.py} +57 -50
- edsl/questions/question_registry.py +1 -1
- edsl/questions/{ResponseValidatorABC.py → response_validator_abc.py} +40 -26
- edsl/questions/response_validator_factory.py +34 -0
- edsl/questions/templates/matrix/__init__.py +1 -0
- edsl/questions/templates/matrix/answering_instructions.jinja +5 -0
- edsl/questions/templates/matrix/question_presentation.jinja +20 -0
- edsl/results/CSSParameterizer.py +1 -1
- edsl/results/Dataset.py +170 -7
- edsl/results/DatasetExportMixin.py +168 -305
- edsl/results/DatasetTree.py +28 -8
- edsl/results/MarkdownToDocx.py +122 -0
- edsl/results/MarkdownToPDF.py +111 -0
- edsl/results/Result.py +298 -206
- edsl/results/Results.py +149 -131
- edsl/results/ResultsExportMixin.py +2 -0
- edsl/results/TableDisplay.py +98 -171
- edsl/results/TextEditor.py +50 -0
- edsl/results/__init__.py +1 -1
- edsl/results/file_exports.py +252 -0
- edsl/results/{Selector.py → results_selector.py} +23 -13
- edsl/results/smart_objects.py +96 -0
- edsl/results/table_data_class.py +12 -0
- edsl/results/table_renderers.py +118 -0
- edsl/scenarios/ConstructDownloadLink.py +109 -0
- edsl/scenarios/DocumentChunker.py +102 -0
- edsl/scenarios/DocxScenario.py +16 -0
- edsl/scenarios/FileStore.py +150 -239
- edsl/scenarios/PdfExtractor.py +40 -0
- edsl/scenarios/Scenario.py +90 -193
- edsl/scenarios/ScenarioHtmlMixin.py +4 -3
- edsl/scenarios/ScenarioList.py +415 -244
- edsl/scenarios/ScenarioListExportMixin.py +0 -7
- edsl/scenarios/ScenarioListPdfMixin.py +15 -37
- edsl/scenarios/__init__.py +1 -2
- edsl/scenarios/directory_scanner.py +96 -0
- edsl/scenarios/file_methods.py +85 -0
- edsl/scenarios/handlers/__init__.py +13 -0
- edsl/scenarios/handlers/csv.py +49 -0
- edsl/scenarios/handlers/docx.py +76 -0
- edsl/scenarios/handlers/html.py +37 -0
- edsl/scenarios/handlers/json.py +111 -0
- edsl/scenarios/handlers/latex.py +5 -0
- edsl/scenarios/handlers/md.py +51 -0
- edsl/scenarios/handlers/pdf.py +68 -0
- edsl/scenarios/handlers/png.py +39 -0
- edsl/scenarios/handlers/pptx.py +105 -0
- edsl/scenarios/handlers/py.py +294 -0
- edsl/scenarios/handlers/sql.py +313 -0
- edsl/scenarios/handlers/sqlite.py +149 -0
- edsl/scenarios/handlers/txt.py +33 -0
- edsl/scenarios/{ScenarioJoin.py → scenario_join.py} +10 -6
- edsl/scenarios/scenario_selector.py +156 -0
- edsl/study/ObjectEntry.py +1 -1
- edsl/study/SnapShot.py +1 -1
- edsl/study/Study.py +5 -12
- edsl/surveys/ConstructDAG.py +92 -0
- edsl/surveys/EditSurvey.py +221 -0
- edsl/surveys/InstructionHandler.py +100 -0
- edsl/surveys/MemoryManagement.py +72 -0
- edsl/surveys/Rule.py +5 -4
- edsl/surveys/RuleCollection.py +25 -27
- edsl/surveys/RuleManager.py +172 -0
- edsl/surveys/Simulator.py +75 -0
- edsl/surveys/Survey.py +270 -791
- edsl/surveys/SurveyCSS.py +20 -8
- edsl/surveys/{SurveyFlowVisualizationMixin.py → SurveyFlowVisualization.py} +11 -9
- edsl/surveys/SurveyToApp.py +141 -0
- edsl/surveys/__init__.py +4 -2
- edsl/surveys/descriptors.py +6 -2
- edsl/surveys/instructions/ChangeInstruction.py +1 -2
- edsl/surveys/instructions/Instruction.py +4 -13
- edsl/surveys/instructions/InstructionCollection.py +11 -6
- edsl/templates/error_reporting/interview_details.html +1 -1
- edsl/templates/error_reporting/report.html +1 -1
- edsl/tools/plotting.py +1 -1
- edsl/utilities/PrettyList.py +56 -0
- edsl/utilities/is_notebook.py +18 -0
- edsl/utilities/is_valid_variable_name.py +11 -0
- edsl/utilities/remove_edsl_version.py +24 -0
- edsl/utilities/utilities.py +35 -23
- {edsl-0.1.38.dev4.dist-info → edsl-0.1.39.dist-info}/METADATA +12 -10
- edsl-0.1.39.dist-info/RECORD +358 -0
- {edsl-0.1.38.dev4.dist-info → edsl-0.1.39.dist-info}/WHEEL +1 -1
- edsl/language_models/KeyLookup.py +0 -30
- edsl/language_models/registry.py +0 -190
- edsl/language_models/unused/ReplicateBase.py +0 -83
- edsl/results/ResultsDBMixin.py +0 -238
- edsl-0.1.38.dev4.dist-info/RECORD +0 -277
- /edsl/questions/{RegisterQuestionsMeta.py → register_questions_meta.py} +0 -0
- /edsl/results/{ResultsFetchMixin.py → results_fetch_mixin.py} +0 -0
- /edsl/results/{ResultsToolsMixin.py → results_tools_mixin.py} +0 -0
- {edsl-0.1.38.dev4.dist-info → edsl-0.1.39.dist-info}/LICENSE +0 -0
edsl/agents/PromptConstructor.py
CHANGED
@@ -1,57 +1,78 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
-
from typing import Dict, Any, Optional, Set
|
3
|
-
|
4
|
-
from jinja2 import Environment, meta
|
2
|
+
from typing import Dict, Any, Optional, Set, Union, TYPE_CHECKING
|
3
|
+
from functools import cached_property
|
5
4
|
|
6
5
|
from edsl.prompts.Prompt import Prompt
|
7
|
-
from edsl.agents.prompt_helpers import PromptPlan
|
8
6
|
|
7
|
+
from dataclasses import dataclass
|
9
8
|
|
10
|
-
|
11
|
-
|
9
|
+
from .prompt_helpers import PromptPlan
|
10
|
+
from .QuestionTemplateReplacementsBuilder import (
|
11
|
+
QuestionTemplateReplacementsBuilder,
|
12
|
+
)
|
13
|
+
from .question_option_processor import QuestionOptionProcessor
|
12
14
|
|
13
|
-
|
14
|
-
|
15
|
+
if TYPE_CHECKING:
|
16
|
+
from edsl.agents.InvigilatorBase import InvigilatorBase
|
17
|
+
from edsl.questions.QuestionBase import QuestionBase
|
18
|
+
from edsl.agents.Agent import Agent
|
19
|
+
from edsl.surveys.Survey import Survey
|
20
|
+
from edsl.language_models.LanguageModel import LanguageModel
|
21
|
+
from edsl.surveys.MemoryPlan import MemoryPlan
|
22
|
+
from edsl.questions.QuestionBase import QuestionBase
|
23
|
+
from edsl.scenarios.Scenario import Scenario
|
24
|
+
|
25
|
+
|
26
|
+
class BasePlaceholder:
|
27
|
+
"""Base class for placeholder values when a question is not yet answered."""
|
28
|
+
|
29
|
+
def __init__(self, placeholder_type: str = "answer"):
|
30
|
+
self.value = "N/A"
|
15
31
|
self.comment = "Will be populated by prior answer"
|
32
|
+
self._type = placeholder_type
|
16
33
|
|
17
34
|
def __getitem__(self, index):
|
18
35
|
return ""
|
19
36
|
|
20
37
|
def __str__(self):
|
21
|
-
return "<<
|
38
|
+
return f"<<{self.__class__.__name__}:{self._type}>>"
|
22
39
|
|
23
40
|
def __repr__(self):
|
24
|
-
return
|
41
|
+
return self.__str__()
|
25
42
|
|
26
43
|
|
27
|
-
|
28
|
-
|
29
|
-
|
44
|
+
class PlaceholderAnswer(BasePlaceholder):
|
45
|
+
def __init__(self):
|
46
|
+
super().__init__("answer")
|
30
47
|
|
31
|
-
Args:
|
32
|
-
template_str (str): The Jinja2 template string
|
33
48
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
49
|
+
class PlaceholderComment(BasePlaceholder):
|
50
|
+
def __init__(self):
|
51
|
+
super().__init__("comment")
|
52
|
+
|
53
|
+
|
54
|
+
class PlaceholderGeneratedTokens(BasePlaceholder):
|
55
|
+
def __init__(self):
|
56
|
+
super().__init__("generated_tokens")
|
40
57
|
|
41
58
|
|
42
59
|
class PromptConstructor:
|
43
60
|
"""
|
61
|
+
This class constructs the prompts for the language model.
|
62
|
+
|
44
63
|
The pieces of a prompt are:
|
45
64
|
- The agent instructions - "You are answering questions as if you were a human. Do not break character."
|
46
65
|
- The persona prompt - "You are an agent with the following persona: {'age': 22, 'hair': 'brown', 'height': 5.5}"
|
47
66
|
- The question instructions - "You are being asked the following question: Do you like school? The options are 0: yes 1: no Return a valid JSON formatted like this, selecting only the number of the option: {"answer": <put answer code here>, "comment": "<put explanation here>"} Only 1 option may be selected."
|
48
67
|
- The memory prompt - "Before the question you are now answering, you already answered the following question(s): Question: Do you like school? Answer: Prior answer"
|
49
|
-
|
50
|
-
This is mixed into the Invigilator class.
|
51
68
|
"""
|
52
69
|
|
53
|
-
def __init__(
|
70
|
+
def __init__(
|
71
|
+
self, invigilator: "InvigilatorBase", prompt_plan: Optional["PromptPlan"] = None
|
72
|
+
):
|
54
73
|
self.invigilator = invigilator
|
74
|
+
self.prompt_plan = prompt_plan or PromptPlan()
|
75
|
+
|
55
76
|
self.agent = invigilator.agent
|
56
77
|
self.question = invigilator.question
|
57
78
|
self.scenario = invigilator.scenario
|
@@ -59,22 +80,12 @@ class PromptConstructor:
|
|
59
80
|
self.model = invigilator.model
|
60
81
|
self.current_answers = invigilator.current_answers
|
61
82
|
self.memory_plan = invigilator.memory_plan
|
62
|
-
self.prompt_plan = prompt_plan or PromptPlan()
|
63
83
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
These will be used to append to the prompt a list of files that are part of the scenario.
|
68
|
-
"""
|
69
|
-
from edsl.scenarios.FileStore import FileStore
|
70
|
-
|
71
|
-
file_entries = []
|
72
|
-
for key, value in self.scenario.items():
|
73
|
-
if isinstance(value, FileStore):
|
74
|
-
file_entries.append(key)
|
75
|
-
return file_entries
|
84
|
+
def get_question_options(self, question_data):
|
85
|
+
"""Get the question options."""
|
86
|
+
return QuestionOptionProcessor(self).get_question_options(question_data)
|
76
87
|
|
77
|
-
@
|
88
|
+
@cached_property
|
78
89
|
def agent_instructions_prompt(self) -> Prompt:
|
79
90
|
"""
|
80
91
|
>>> from edsl.agents.InvigilatorBase import InvigilatorBase
|
@@ -82,14 +93,14 @@ class PromptConstructor:
|
|
82
93
|
>>> i.prompt_constructor.agent_instructions_prompt
|
83
94
|
Prompt(text=\"""You are answering questions as if you were a human. Do not break character.\""")
|
84
95
|
"""
|
85
|
-
from edsl import Agent
|
96
|
+
from edsl.agents.Agent import Agent
|
86
97
|
|
87
98
|
if self.agent == Agent(): # if agent is empty, then return an empty prompt
|
88
99
|
return Prompt(text="")
|
89
100
|
|
90
101
|
return Prompt(text=self.agent.instruction)
|
91
102
|
|
92
|
-
@
|
103
|
+
@cached_property
|
93
104
|
def agent_persona_prompt(self) -> Prompt:
|
94
105
|
"""
|
95
106
|
>>> from edsl.agents.InvigilatorBase import InvigilatorBase
|
@@ -97,159 +108,96 @@ class PromptConstructor:
|
|
97
108
|
>>> i.prompt_constructor.agent_persona_prompt
|
98
109
|
Prompt(text=\"""Your traits: {'age': 22, 'hair': 'brown', 'height': 5.5}\""")
|
99
110
|
"""
|
100
|
-
from edsl import Agent
|
111
|
+
from edsl.agents.Agent import Agent
|
101
112
|
|
102
113
|
if self.agent == Agent(): # if agent is empty, then return an empty prompt
|
103
114
|
return Prompt(text="")
|
104
115
|
|
105
116
|
return self.agent.prompt()
|
106
117
|
|
107
|
-
def prior_answers_dict(self) -> dict:
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
d[question].answer = PlaceholderAnswer()
|
116
|
-
|
117
|
-
# if (new_question := question.split("_comment")[0]) in d:
|
118
|
-
# d[new_question].comment = answer
|
119
|
-
# d[question].answer = PlaceholderAnswer()
|
120
|
-
|
121
|
-
# breakpoint()
|
122
|
-
return d
|
123
|
-
|
124
|
-
@property
|
125
|
-
def question_file_keys(self):
|
126
|
-
raw_question_text = self.question.question_text
|
127
|
-
variables = get_jinja2_variables(raw_question_text)
|
128
|
-
question_file_keys = []
|
129
|
-
for var in variables:
|
130
|
-
if var in self.scenario_file_keys:
|
131
|
-
question_file_keys.append(var)
|
132
|
-
return question_file_keys
|
133
|
-
|
134
|
-
def build_replacement_dict(self, question_data: dict):
|
118
|
+
def prior_answers_dict(self) -> dict[str, "QuestionBase"]:
|
119
|
+
"""This is a dictionary of prior answers, if they exist."""
|
120
|
+
return self._add_answers(
|
121
|
+
self.survey.question_names_to_questions(), self.current_answers
|
122
|
+
)
|
123
|
+
|
124
|
+
@staticmethod
|
125
|
+
def _extract_quetion_and_entry_type(key_entry) -> tuple[str, str]:
|
135
126
|
"""
|
136
|
-
|
127
|
+
Extracts the question name and type for the current answer dictionary key entry.
|
128
|
+
|
129
|
+
>>> PromptConstructor._extract_quetion_and_entry_type("q0")
|
130
|
+
('q0', 'answer')
|
131
|
+
>>> PromptConstructor._extract_quetion_and_entry_type("q0_comment")
|
132
|
+
('q0', 'comment')
|
133
|
+
>>> PromptConstructor._extract_quetion_and_entry_type("q0_alternate_generated_tokens")
|
134
|
+
('q0_alternate', 'generated_tokens')
|
135
|
+
>>> PromptConstructor._extract_quetion_and_entry_type("q0_alt_comment")
|
136
|
+
('q0_alt', 'comment')
|
137
137
|
"""
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
replacement_dict = {}
|
154
|
-
for d in [
|
155
|
-
file_refs,
|
156
|
-
question_data,
|
157
|
-
scenario_items,
|
158
|
-
self.prior_answers_dict(),
|
159
|
-
{"agent": self.agent},
|
160
|
-
question_settings,
|
161
|
-
]:
|
162
|
-
replacement_dict.update(d)
|
163
|
-
|
164
|
-
return replacement_dict
|
165
|
-
|
166
|
-
def _get_question_options(self, question_data):
|
167
|
-
question_options_entry = question_data.get("question_options", None)
|
168
|
-
question_options = question_options_entry
|
169
|
-
|
170
|
-
placeholder = ["<< Option 1 - Placholder >>", "<< Option 2 - Placholder >>"]
|
171
|
-
|
172
|
-
# print("Question options entry: ", question_options_entry)
|
173
|
-
|
174
|
-
if isinstance(question_options_entry, str):
|
175
|
-
env = Environment()
|
176
|
-
parsed_content = env.parse(question_options_entry)
|
177
|
-
question_option_key = list(meta.find_undeclared_variables(parsed_content))[
|
178
|
-
0
|
179
|
-
]
|
180
|
-
if isinstance(self.scenario.get(question_option_key), list):
|
181
|
-
question_options = self.scenario.get(question_option_key)
|
182
|
-
|
183
|
-
# might be getting it from the prior answers
|
184
|
-
if self.prior_answers_dict().get(question_option_key) is not None:
|
185
|
-
prior_question = self.prior_answers_dict().get(question_option_key)
|
186
|
-
if hasattr(prior_question, "answer"):
|
187
|
-
if isinstance(prior_question.answer, list):
|
188
|
-
question_options = prior_question.answer
|
189
|
-
else:
|
190
|
-
question_options = placeholder
|
191
|
-
else:
|
192
|
-
question_options = placeholder
|
193
|
-
|
194
|
-
return question_options
|
195
|
-
|
196
|
-
def build_question_instructions_prompt(self):
|
197
|
-
"""Buils the question instructions prompt."""
|
198
|
-
|
199
|
-
question_prompt = Prompt(self.question.get_instructions(model=self.model.model))
|
200
|
-
|
201
|
-
# Get the data for the question - this is a dictionary of the question data
|
202
|
-
# e.g., {'question_text': 'Do you like school?', 'question_name': 'q0', 'question_options': ['yes', 'no']}
|
203
|
-
question_data = self.question.data.copy()
|
204
|
-
|
205
|
-
if (
|
206
|
-
"question_options" in question_data
|
207
|
-
): # is this a question with question options?
|
208
|
-
question_options = self._get_question_options(question_data)
|
209
|
-
question_data["question_options"] = question_options
|
210
|
-
|
211
|
-
replacement_dict = self.build_replacement_dict(question_data)
|
212
|
-
rendered_instructions = question_prompt.render(replacement_dict)
|
138
|
+
split_list = key_entry.rsplit("_", maxsplit=1)
|
139
|
+
if len(split_list) == 1:
|
140
|
+
question_name = split_list[0]
|
141
|
+
entry_type = "answer"
|
142
|
+
else:
|
143
|
+
if split_list[1] == "comment":
|
144
|
+
question_name = split_list[0]
|
145
|
+
entry_type = "comment"
|
146
|
+
elif split_list[1] == "tokens": # it's actually 'generated_tokens'
|
147
|
+
question_name = key_entry.replace("_generated_tokens", "")
|
148
|
+
entry_type = "generated_tokens"
|
149
|
+
else:
|
150
|
+
question_name = key_entry
|
151
|
+
entry_type = "answer"
|
152
|
+
return question_name, entry_type
|
213
153
|
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
)
|
154
|
+
@staticmethod
|
155
|
+
def _augmented_answers_dict(current_answers: dict) -> dict:
|
156
|
+
"""
|
157
|
+
>>> PromptConstructor._augmented_answers_dict({"q0": "LOVE IT!", "q0_comment": "I love school!"})
|
158
|
+
{'q0': {'answer': 'LOVE IT!', 'comment': 'I love school!'}}
|
159
|
+
"""
|
160
|
+
from collections import defaultdict
|
218
161
|
|
219
|
-
|
220
|
-
for
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
if undefined_template_variables:
|
228
|
-
msg = f"Question instructions still has variables: {undefined_template_variables}."
|
229
|
-
import warnings
|
230
|
-
|
231
|
-
warnings.warn(msg)
|
232
|
-
# raise QuestionScenarioRenderError(
|
233
|
-
# f"Question instructions still has variables: {undefined_template_variables}."
|
234
|
-
# )
|
235
|
-
|
236
|
-
# Check if question has instructions - these are instructions in a Survey that can apply to multiple follow-on questions
|
237
|
-
relevant_instructions = self.survey.relevant_instructions(
|
238
|
-
self.question.question_name
|
239
|
-
)
|
162
|
+
d = defaultdict(dict)
|
163
|
+
for key, value in current_answers.items():
|
164
|
+
question_name, entry_type = (
|
165
|
+
PromptConstructor._extract_quetion_and_entry_type(key)
|
166
|
+
)
|
167
|
+
d[question_name][entry_type] = value
|
168
|
+
return dict(d)
|
240
169
|
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
170
|
+
@staticmethod
|
171
|
+
def _add_answers(
|
172
|
+
answer_dict: dict, current_answers: dict
|
173
|
+
) -> dict[str, "QuestionBase"]:
|
174
|
+
"""
|
175
|
+
>>> from edsl import QuestionFreeText
|
176
|
+
>>> d = {"q0": QuestionFreeText(question_text="Do you like school?", question_name = "q0")}
|
177
|
+
>>> current_answers = {"q0": "LOVE IT!"}
|
178
|
+
>>> PromptConstructor._add_answers(d, current_answers)['q0'].answer
|
179
|
+
'LOVE IT!'
|
180
|
+
"""
|
181
|
+
augmented_answers = PromptConstructor._augmented_answers_dict(current_answers)
|
249
182
|
|
250
|
-
|
183
|
+
for question in answer_dict:
|
184
|
+
if question in augmented_answers:
|
185
|
+
for entry_type, value in augmented_answers[question].items():
|
186
|
+
setattr(answer_dict[question], entry_type, value)
|
187
|
+
else:
|
188
|
+
answer_dict[question].answer = PlaceholderAnswer()
|
189
|
+
answer_dict[question].comment = PlaceholderComment()
|
190
|
+
answer_dict[question].generated_tokens = PlaceholderGeneratedTokens()
|
191
|
+
return answer_dict
|
192
|
+
|
193
|
+
@cached_property
|
194
|
+
def question_file_keys(self) -> list:
|
195
|
+
"""Extracts the file keys from the question text.
|
196
|
+
It checks if the variables in the question text are in the scenario file keys.
|
197
|
+
"""
|
198
|
+
return QuestionTemplateReplacementsBuilder(self).question_file_keys()
|
251
199
|
|
252
|
-
@
|
200
|
+
@cached_property
|
253
201
|
def question_instructions_prompt(self) -> Prompt:
|
254
202
|
"""
|
255
203
|
>>> from edsl.agents.InvigilatorBase import InvigilatorBase
|
@@ -258,25 +206,24 @@ class PromptConstructor:
|
|
258
206
|
Prompt(text=\"""...
|
259
207
|
...
|
260
208
|
"""
|
261
|
-
|
262
|
-
self._question_instructions_prompt = (
|
263
|
-
self.build_question_instructions_prompt()
|
264
|
-
)
|
209
|
+
return self.build_question_instructions_prompt()
|
265
210
|
|
266
|
-
|
211
|
+
def build_question_instructions_prompt(self) -> Prompt:
|
212
|
+
"""Buils the question instructions prompt."""
|
213
|
+
from edsl.agents.QuestionInstructionPromptBuilder import (
|
214
|
+
QuestionInstructionPromptBuilder,
|
215
|
+
)
|
267
216
|
|
268
|
-
|
269
|
-
def prior_question_memory_prompt(self) -> Prompt:
|
270
|
-
if not hasattr(self, "_prior_question_memory_prompt"):
|
271
|
-
from edsl.prompts.Prompt import Prompt
|
217
|
+
return QuestionInstructionPromptBuilder(self).build()
|
272
218
|
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
219
|
+
@cached_property
|
220
|
+
def prior_question_memory_prompt(self) -> Prompt:
|
221
|
+
memory_prompt = Prompt(text="")
|
222
|
+
if self.memory_plan is not None:
|
223
|
+
memory_prompt += self.create_memory_prompt(
|
224
|
+
self.question.question_name
|
225
|
+
).render(self.scenario | self.prior_answers_dict())
|
226
|
+
return memory_prompt
|
280
227
|
|
281
228
|
def create_memory_prompt(self, question_name: str) -> Prompt:
|
282
229
|
"""Create a memory for the agent.
|
@@ -295,24 +242,6 @@ class PromptConstructor:
|
|
295
242
|
question_name, self.current_answers
|
296
243
|
)
|
297
244
|
|
298
|
-
def construct_system_prompt(self) -> Prompt:
|
299
|
-
"""Construct the system prompt for the LLM call."""
|
300
|
-
import warnings
|
301
|
-
|
302
|
-
warnings.warn(
|
303
|
-
"This method is deprecated. Use get_prompts instead.", DeprecationWarning
|
304
|
-
)
|
305
|
-
return self.get_prompts()["system_prompt"]
|
306
|
-
|
307
|
-
def construct_user_prompt(self) -> Prompt:
|
308
|
-
"""Construct the user prompt for the LLM call."""
|
309
|
-
import warnings
|
310
|
-
|
311
|
-
warnings.warn(
|
312
|
-
"This method is deprecated. Use get_prompts instead.", DeprecationWarning
|
313
|
-
)
|
314
|
-
return self.get_prompts()["user_prompt"]
|
315
|
-
|
316
245
|
def get_prompts(self) -> Dict[str, Prompt]:
|
317
246
|
"""Get both prompts for the LLM call.
|
318
247
|
|
@@ -323,7 +252,6 @@ class PromptConstructor:
|
|
323
252
|
>>> i.get_prompts()
|
324
253
|
{'user_prompt': ..., 'system_prompt': ...}
|
325
254
|
"""
|
326
|
-
# breakpoint()
|
327
255
|
prompts = self.prompt_plan.get_prompts(
|
328
256
|
agent_instructions=self.agent_instructions_prompt,
|
329
257
|
agent_persona=self.agent_persona_prompt,
|
@@ -337,16 +265,6 @@ class PromptConstructor:
|
|
337
265
|
prompts["files_list"] = files_list
|
338
266
|
return prompts
|
339
267
|
|
340
|
-
def _get_scenario_with_image(self) -> Scenario:
|
341
|
-
"""This is a helper function to get a scenario with an image, for testing purposes."""
|
342
|
-
from edsl import Scenario
|
343
|
-
|
344
|
-
try:
|
345
|
-
scenario = Scenario.from_image("../../static/logo.png")
|
346
|
-
except FileNotFoundError:
|
347
|
-
scenario = Scenario.from_image("static/logo.png")
|
348
|
-
return scenario
|
349
|
-
|
350
268
|
|
351
269
|
if __name__ == "__main__":
|
352
270
|
import doctest
|
@@ -0,0 +1,128 @@
|
|
1
|
+
from typing import Dict, List, Set
|
2
|
+
from warnings import warn
|
3
|
+
from edsl.prompts.Prompt import Prompt
|
4
|
+
|
5
|
+
from edsl.agents.QuestionTemplateReplacementsBuilder import (
|
6
|
+
QuestionTemplateReplacementsBuilder as QTRB,
|
7
|
+
)
|
8
|
+
|
9
|
+
|
10
|
+
class QuestionInstructionPromptBuilder:
|
11
|
+
"""Handles the construction and rendering of question instructions."""
|
12
|
+
|
13
|
+
def __init__(self, prompt_constructor: "PromptConstructor"):
|
14
|
+
self.prompt_constructor = prompt_constructor
|
15
|
+
|
16
|
+
self.model = self.prompt_constructor.model
|
17
|
+
self.survey = self.prompt_constructor.survey
|
18
|
+
self.question = self.prompt_constructor.question
|
19
|
+
|
20
|
+
def build(self) -> Prompt:
|
21
|
+
"""Builds the complete question instructions prompt with all necessary components.
|
22
|
+
|
23
|
+
Returns:
|
24
|
+
Prompt: The fully rendered question instructions
|
25
|
+
"""
|
26
|
+
base_prompt = self._create_base_prompt()
|
27
|
+
enriched_prompt = self._enrich_with_question_options(base_prompt)
|
28
|
+
rendered_prompt = self._render_prompt(enriched_prompt)
|
29
|
+
self._validate_template_variables(rendered_prompt)
|
30
|
+
|
31
|
+
return self._append_survey_instructions(rendered_prompt)
|
32
|
+
|
33
|
+
def _create_base_prompt(self) -> Dict:
|
34
|
+
"""Creates the initial prompt with basic question data.
|
35
|
+
|
36
|
+
Returns:
|
37
|
+
Dict: Base question data
|
38
|
+
"""
|
39
|
+
return {
|
40
|
+
"prompt": Prompt(self.question.get_instructions(model=self.model.model)),
|
41
|
+
"data": self.question.data.copy(),
|
42
|
+
}
|
43
|
+
|
44
|
+
def _enrich_with_question_options(self, prompt_data: Dict) -> Dict:
|
45
|
+
"""Enriches the prompt data with question options if they exist.
|
46
|
+
|
47
|
+
Args:
|
48
|
+
prompt_data: Dictionary containing prompt and question data
|
49
|
+
|
50
|
+
Returns:
|
51
|
+
Dict: Enriched prompt data
|
52
|
+
"""
|
53
|
+
if "question_options" in prompt_data["data"]:
|
54
|
+
from edsl.agents.question_option_processor import QuestionOptionProcessor
|
55
|
+
|
56
|
+
question_options = QuestionOptionProcessor(
|
57
|
+
self.prompt_constructor
|
58
|
+
).get_question_options(question_data=prompt_data["data"])
|
59
|
+
|
60
|
+
prompt_data["data"]["question_options"] = question_options
|
61
|
+
return prompt_data
|
62
|
+
|
63
|
+
def _render_prompt(self, prompt_data: Dict) -> Prompt:
|
64
|
+
"""Renders the prompt using the replacement dictionary.
|
65
|
+
|
66
|
+
Args:
|
67
|
+
prompt_data: Dictionary containing prompt and question data
|
68
|
+
|
69
|
+
Returns:
|
70
|
+
Prompt: Rendered instructions
|
71
|
+
"""
|
72
|
+
|
73
|
+
replacement_dict = QTRB(self.prompt_constructor).build_replacement_dict(
|
74
|
+
prompt_data["data"]
|
75
|
+
)
|
76
|
+
return prompt_data["prompt"].render(replacement_dict)
|
77
|
+
|
78
|
+
def _validate_template_variables(self, rendered_prompt: Prompt) -> None:
|
79
|
+
"""Validates that all template variables have been properly replaced.
|
80
|
+
|
81
|
+
Args:
|
82
|
+
rendered_prompt: The rendered prompt to validate
|
83
|
+
|
84
|
+
Warns:
|
85
|
+
If any template variables remain undefined
|
86
|
+
"""
|
87
|
+
undefined_vars = rendered_prompt.undefined_template_variables({})
|
88
|
+
|
89
|
+
# Check for question names in undefined variables
|
90
|
+
self._check_question_names_in_undefined_vars(undefined_vars)
|
91
|
+
|
92
|
+
# Warn about any remaining undefined variables
|
93
|
+
if undefined_vars:
|
94
|
+
warn(f"Question instructions still has variables: {undefined_vars}.")
|
95
|
+
|
96
|
+
def _check_question_names_in_undefined_vars(self, undefined_vars: Set[str]) -> None:
|
97
|
+
"""Checks if any undefined variables match question names in the survey.
|
98
|
+
|
99
|
+
Args:
|
100
|
+
undefined_vars: Set of undefined template variables
|
101
|
+
"""
|
102
|
+
for question_name in self.survey.question_names:
|
103
|
+
if question_name in undefined_vars:
|
104
|
+
print(
|
105
|
+
f"Question name found in undefined_template_variables: {question_name}"
|
106
|
+
)
|
107
|
+
|
108
|
+
def _append_survey_instructions(self, rendered_prompt: Prompt) -> Prompt:
|
109
|
+
"""Appends any relevant survey instructions to the rendered prompt.
|
110
|
+
|
111
|
+
Args:
|
112
|
+
rendered_prompt: The rendered prompt to append instructions to
|
113
|
+
|
114
|
+
Returns:
|
115
|
+
Prompt: Final prompt with survey instructions
|
116
|
+
"""
|
117
|
+
relevant_instructions = self.survey._relevant_instructions(
|
118
|
+
self.question.question_name
|
119
|
+
)
|
120
|
+
|
121
|
+
if not relevant_instructions:
|
122
|
+
return rendered_prompt
|
123
|
+
|
124
|
+
preamble = Prompt(text="")
|
125
|
+
for instruction in relevant_instructions:
|
126
|
+
preamble += instruction.text
|
127
|
+
|
128
|
+
return preamble + rendered_prompt
|