edsl 0.1.39.dev1__py3-none-any.whl → 0.1.39.dev2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- edsl/Base.py +169 -116
- edsl/__init__.py +14 -6
- edsl/__version__.py +1 -1
- edsl/agents/Agent.py +358 -146
- edsl/agents/AgentList.py +211 -73
- edsl/agents/Invigilator.py +88 -36
- edsl/agents/InvigilatorBase.py +59 -70
- edsl/agents/PromptConstructor.py +117 -219
- edsl/agents/QuestionInstructionPromptBuilder.py +128 -0
- edsl/agents/QuestionOptionProcessor.py +172 -0
- edsl/agents/QuestionTemplateReplacementsBuilder.py +137 -0
- edsl/agents/__init__.py +0 -1
- edsl/agents/prompt_helpers.py +3 -3
- 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 +104 -42
- edsl/coop/utils.py +14 -14
- edsl/data/Cache.py +21 -14
- edsl/data/CacheEntry.py +12 -15
- edsl/data/CacheHandler.py +33 -12
- edsl/data/__init__.py +4 -3
- edsl/data_transfer_models.py +2 -1
- edsl/enums.py +20 -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 +0 -3
- edsl/inference_services/AvailableModelCacheHandler.py +184 -0
- edsl/inference_services/AvailableModelFetcher.py +209 -0
- edsl/inference_services/AwsBedrock.py +0 -2
- edsl/inference_services/AzureAI.py +0 -2
- edsl/inference_services/GoogleService.py +2 -11
- edsl/inference_services/InferenceServiceABC.py +18 -85
- edsl/inference_services/InferenceServicesCollection.py +105 -80
- edsl/inference_services/MistralAIService.py +0 -3
- edsl/inference_services/OpenAIService.py +1 -4
- edsl/inference_services/PerplexityService.py +0 -3
- edsl/inference_services/ServiceAvailability.py +135 -0
- edsl/inference_services/TestService.py +11 -8
- edsl/inference_services/data_structures.py +62 -0
- edsl/jobs/AnswerQuestionFunctionConstructor.py +188 -0
- edsl/jobs/Answers.py +1 -14
- edsl/jobs/FetchInvigilator.py +40 -0
- edsl/jobs/InterviewTaskManager.py +98 -0
- edsl/jobs/InterviewsConstructor.py +48 -0
- edsl/jobs/Jobs.py +102 -243
- edsl/jobs/JobsChecks.py +35 -10
- edsl/jobs/JobsComponentConstructor.py +189 -0
- edsl/jobs/JobsPrompts.py +5 -3
- edsl/jobs/JobsRemoteInferenceHandler.py +128 -80
- edsl/jobs/JobsRemoteInferenceLogger.py +239 -0
- edsl/jobs/RequestTokenEstimator.py +30 -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/decorators.py +35 -0
- edsl/jobs/interviews/Interview.py +77 -380
- edsl/jobs/jobs_status_enums.py +9 -0
- edsl/jobs/loggers/HTMLTableJobLogger.py +304 -0
- edsl/jobs/runners/JobsRunnerAsyncio.py +4 -49
- edsl/jobs/tasks/QuestionTaskCreator.py +21 -19
- edsl/jobs/tasks/TaskHistory.py +14 -15
- edsl/jobs/tasks/task_status_enum.py +0 -2
- edsl/language_models/ComputeCost.py +63 -0
- edsl/language_models/LanguageModel.py +137 -234
- edsl/language_models/ModelList.py +11 -13
- 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 +0 -1
- 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/registry.py +49 -59
- 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/AnswerValidatorMixin.py +47 -2
- edsl/questions/ExceptionExplainer.py +77 -0
- edsl/questions/HTMLQuestion.py +103 -0
- edsl/questions/LoopProcessor.py +149 -0
- edsl/questions/QuestionBase.py +37 -192
- edsl/questions/QuestionBaseGenMixin.py +52 -48
- edsl/questions/QuestionBasePromptsMixin.py +7 -3
- edsl/questions/QuestionCheckBox.py +1 -1
- edsl/questions/QuestionExtract.py +1 -1
- edsl/questions/QuestionFreeText.py +1 -2
- edsl/questions/QuestionList.py +3 -5
- edsl/questions/QuestionMatrix.py +265 -0
- edsl/questions/QuestionMultipleChoice.py +66 -22
- edsl/questions/QuestionNumerical.py +1 -3
- edsl/questions/QuestionRank.py +6 -16
- edsl/questions/ResponseValidatorABC.py +37 -11
- edsl/questions/ResponseValidatorFactory.py +28 -0
- edsl/questions/SimpleAskMixin.py +4 -3
- edsl/questions/__init__.py +1 -0
- edsl/questions/derived/QuestionLinearScale.py +6 -3
- edsl/questions/derived/QuestionTopK.py +1 -1
- edsl/questions/descriptors.py +17 -3
- edsl/questions/question_registry.py +1 -1
- 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 +224 -302
- edsl/results/DatasetTree.py +28 -8
- edsl/results/MarkdownToDocx.py +122 -0
- edsl/results/MarkdownToPDF.py +111 -0
- edsl/results/Result.py +192 -206
- edsl/results/Results.py +120 -113
- edsl/results/ResultsExportMixin.py +2 -0
- edsl/results/Selector.py +23 -13
- edsl/results/TableDisplay.py +98 -171
- edsl/results/TextEditor.py +50 -0
- edsl/results/__init__.py +1 -1
- 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/DirectoryScanner.py +96 -0
- edsl/scenarios/DocumentChunker.py +102 -0
- edsl/scenarios/DocxScenario.py +16 -0
- edsl/scenarios/FileStore.py +118 -239
- edsl/scenarios/PdfExtractor.py +40 -0
- edsl/scenarios/Scenario.py +90 -193
- edsl/scenarios/ScenarioHtmlMixin.py +4 -3
- edsl/scenarios/ScenarioJoin.py +10 -6
- edsl/scenarios/ScenarioList.py +383 -240
- edsl/scenarios/ScenarioListExportMixin.py +0 -7
- edsl/scenarios/ScenarioListPdfMixin.py +15 -37
- edsl/scenarios/ScenarioSelector.py +156 -0
- edsl/scenarios/__init__.py +1 -2
- edsl/scenarios/file_methods.py +85 -0
- edsl/scenarios/handlers/__init__.py +13 -0
- edsl/scenarios/handlers/csv.py +38 -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/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 +199 -771
- 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.39.dev1.dist-info → edsl-0.1.39.dev2.dist-info}/METADATA +12 -10
- edsl-0.1.39.dev2.dist-info/RECORD +352 -0
- edsl/language_models/KeyLookup.py +0 -30
- edsl/language_models/unused/ReplicateBase.py +0 -83
- edsl/results/ResultsDBMixin.py +0 -238
- edsl-0.1.39.dev1.dist-info/RECORD +0 -277
- {edsl-0.1.39.dev1.dist-info → edsl-0.1.39.dev2.dist-info}/LICENSE +0 -0
- {edsl-0.1.39.dev1.dist-info → edsl-0.1.39.dev2.dist-info}/WHEEL +0 -0
@@ -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.QuestionOptionProcessor 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
|
@@ -0,0 +1,172 @@
|
|
1
|
+
from jinja2 import Environment, meta
|
2
|
+
from typing import List, Optional, Union
|
3
|
+
|
4
|
+
|
5
|
+
class QuestionOptionProcessor:
|
6
|
+
"""
|
7
|
+
Class that manages the processing of question options.
|
8
|
+
These can be provided directly, as a template string, or fetched from prior answers or the scenario.
|
9
|
+
"""
|
10
|
+
|
11
|
+
def __init__(self, prompt_constructor):
|
12
|
+
self.prompt_constructor = prompt_constructor
|
13
|
+
|
14
|
+
@staticmethod
|
15
|
+
def _get_default_options() -> list:
|
16
|
+
"""Return default placeholder options."""
|
17
|
+
return [f"<< Option {i} - Placeholder >>" for i in range(1, 4)]
|
18
|
+
|
19
|
+
@staticmethod
|
20
|
+
def _parse_template_variable(template_str: str) -> str:
|
21
|
+
"""
|
22
|
+
Extract the variable name from a template string.
|
23
|
+
|
24
|
+
Args:
|
25
|
+
template_str (str): Jinja template string
|
26
|
+
|
27
|
+
Returns:
|
28
|
+
str: Name of the first undefined variable in the template
|
29
|
+
|
30
|
+
>>> QuestionOptionProcessor._parse_template_variable("Here are some {{ options }}")
|
31
|
+
'options'
|
32
|
+
>>> QuestionOptionProcessor._parse_template_variable("Here are some {{ options }} and {{ other }}")
|
33
|
+
Traceback (most recent call last):
|
34
|
+
...
|
35
|
+
ValueError: Multiple variables found in template string
|
36
|
+
>>> QuestionOptionProcessor._parse_template_variable("Here are some")
|
37
|
+
Traceback (most recent call last):
|
38
|
+
...
|
39
|
+
ValueError: No variables found in template string
|
40
|
+
"""
|
41
|
+
env = Environment()
|
42
|
+
parsed_content = env.parse(template_str)
|
43
|
+
undeclared_variables = list(meta.find_undeclared_variables(parsed_content))
|
44
|
+
if not undeclared_variables:
|
45
|
+
raise ValueError("No variables found in template string")
|
46
|
+
if len(undeclared_variables) > 1:
|
47
|
+
raise ValueError("Multiple variables found in template string")
|
48
|
+
return undeclared_variables[0]
|
49
|
+
|
50
|
+
@staticmethod
|
51
|
+
def _get_options_from_scenario(
|
52
|
+
scenario: dict, option_key: str
|
53
|
+
) -> Union[list, None]:
|
54
|
+
"""
|
55
|
+
Try to get options from scenario data.
|
56
|
+
|
57
|
+
>>> from edsl import Scenario
|
58
|
+
>>> scenario = Scenario({"options": ["Option 1", "Option 2"]})
|
59
|
+
>>> QuestionOptionProcessor._get_options_from_scenario(scenario, "options")
|
60
|
+
['Option 1', 'Option 2']
|
61
|
+
|
62
|
+
|
63
|
+
Returns:
|
64
|
+
list | None: List of options if found in scenario, None otherwise
|
65
|
+
"""
|
66
|
+
scenario_options = scenario.get(option_key)
|
67
|
+
return scenario_options if isinstance(scenario_options, list) else None
|
68
|
+
|
69
|
+
@staticmethod
|
70
|
+
def _get_options_from_prior_answers(
|
71
|
+
prior_answers: dict, option_key: str
|
72
|
+
) -> Union[list, None]:
|
73
|
+
"""
|
74
|
+
Try to get options from prior answers.
|
75
|
+
|
76
|
+
prior_answers (dict): Dictionary of prior answers
|
77
|
+
option_key (str): Key to look up in prior answers
|
78
|
+
|
79
|
+
>>> from edsl import QuestionList as Q
|
80
|
+
>>> q = Q.example()
|
81
|
+
>>> q.answer = ["Option 1", "Option 2"]
|
82
|
+
>>> prior_answers = {"options": q}
|
83
|
+
>>> QuestionOptionProcessor._get_options_from_prior_answers(prior_answers, "options")
|
84
|
+
['Option 1', 'Option 2']
|
85
|
+
>>> QuestionOptionProcessor._get_options_from_prior_answers(prior_answers, "wrong_key") is None
|
86
|
+
True
|
87
|
+
|
88
|
+
Returns:
|
89
|
+
list | None: List of options if found in prior answers, None otherwise
|
90
|
+
"""
|
91
|
+
prior_answer = prior_answers.get(option_key)
|
92
|
+
if prior_answer and hasattr(prior_answer, "answer"):
|
93
|
+
if isinstance(prior_answer.answer, list):
|
94
|
+
return prior_answer.answer
|
95
|
+
return None
|
96
|
+
|
97
|
+
def get_question_options(self, question_data: dict) -> list:
|
98
|
+
"""
|
99
|
+
Extract and process question options from question data.
|
100
|
+
|
101
|
+
Args:
|
102
|
+
question_data (dict): Dictionary containing question configuration
|
103
|
+
|
104
|
+
Returns:
|
105
|
+
list: List of question options. Returns default placeholders if no valid options found.
|
106
|
+
|
107
|
+
>>> class MockPromptConstructor:
|
108
|
+
... pass
|
109
|
+
>>> mpc = MockPromptConstructor()
|
110
|
+
>>> from edsl import Scenario
|
111
|
+
>>> mpc.scenario = Scenario({"options": ["Option 1", "Option 2"]})
|
112
|
+
>>> processor = QuestionOptionProcessor(mpc)
|
113
|
+
|
114
|
+
The basic case where options are directly provided:
|
115
|
+
|
116
|
+
>>> question_data = {"question_options": ["Option 1", "Option 2"]}
|
117
|
+
>>> processor.get_question_options(question_data)
|
118
|
+
['Option 1', 'Option 2']
|
119
|
+
|
120
|
+
The case where options are provided as a template string:
|
121
|
+
|
122
|
+
>>> question_data = {"question_options": "{{ options }}"}
|
123
|
+
>>> processor.get_question_options(question_data)
|
124
|
+
['Option 1', 'Option 2']
|
125
|
+
|
126
|
+
The case where there is a templace string but it's in the prior answers:
|
127
|
+
|
128
|
+
>>> class MockQuestion:
|
129
|
+
... pass
|
130
|
+
>>> q0 = MockQuestion()
|
131
|
+
>>> q0.answer = ["Option 1", "Option 2"]
|
132
|
+
>>> mpc.prior_answers_dict = lambda: {'q0': q0}
|
133
|
+
>>> processor = QuestionOptionProcessor(mpc)
|
134
|
+
>>> question_data = {"question_options": "{{ q0 }}"}
|
135
|
+
>>> processor.get_question_options(question_data)
|
136
|
+
['Option 1', 'Option 2']
|
137
|
+
|
138
|
+
The case we're no options are found:
|
139
|
+
>>> processor.get_question_options({"question_options": "{{ poop }}"})
|
140
|
+
['<< Option 1 - Placeholder >>', '<< Option 2 - Placeholder >>', '<< Option 3 - Placeholder >>']
|
141
|
+
|
142
|
+
"""
|
143
|
+
options_entry = question_data.get("question_options")
|
144
|
+
|
145
|
+
# If not a template string, return as is or default
|
146
|
+
if not isinstance(options_entry, str):
|
147
|
+
return options_entry if options_entry else self._get_default_options()
|
148
|
+
|
149
|
+
# Parse template to get variable name
|
150
|
+
option_key = self._parse_template_variable(options_entry)
|
151
|
+
|
152
|
+
# Try getting options from scenario
|
153
|
+
scenario_options = self._get_options_from_scenario(
|
154
|
+
self.prompt_constructor.scenario, option_key
|
155
|
+
)
|
156
|
+
if scenario_options:
|
157
|
+
return scenario_options
|
158
|
+
|
159
|
+
# Try getting options from prior answers
|
160
|
+
prior_answer_options = self._get_options_from_prior_answers(
|
161
|
+
self.prompt_constructor.prior_answers_dict(), option_key
|
162
|
+
)
|
163
|
+
if prior_answer_options:
|
164
|
+
return prior_answer_options
|
165
|
+
|
166
|
+
return self._get_default_options()
|
167
|
+
|
168
|
+
|
169
|
+
if __name__ == "__main__":
|
170
|
+
import doctest
|
171
|
+
|
172
|
+
doctest.testmod()
|
@@ -0,0 +1,137 @@
|
|
1
|
+
from jinja2 import Environment, meta
|
2
|
+
from typing import Any, Set, TYPE_CHECKING
|
3
|
+
|
4
|
+
if TYPE_CHECKING:
|
5
|
+
from edsl.agents.PromptConstructor import PromptConstructor
|
6
|
+
from edsl.scenarios.Scenario import Scenario
|
7
|
+
|
8
|
+
|
9
|
+
class QuestionTemplateReplacementsBuilder:
|
10
|
+
def __init__(self, prompt_constructor: "PromptConstructor"):
|
11
|
+
self.prompt_constructor = prompt_constructor
|
12
|
+
|
13
|
+
def question_file_keys(self):
|
14
|
+
question_text = self.prompt_constructor.question.question_text
|
15
|
+
file_keys = self._find_file_keys(self.prompt_constructor.scenario)
|
16
|
+
return self._extract_file_keys_from_question_text(question_text, file_keys)
|
17
|
+
|
18
|
+
def scenario_file_keys(self):
|
19
|
+
return self._find_file_keys(self.prompt_constructor.scenario)
|
20
|
+
|
21
|
+
def get_jinja2_variables(template_str: str) -> Set[str]:
|
22
|
+
"""
|
23
|
+
Extracts all variable names from a Jinja2 template using Jinja2's built-in parsing.
|
24
|
+
|
25
|
+
Args:
|
26
|
+
template_str (str): The Jinja2 template string
|
27
|
+
|
28
|
+
Returns:
|
29
|
+
Set[str]: A set of variable names found in the template
|
30
|
+
"""
|
31
|
+
env = Environment()
|
32
|
+
ast = env.parse(template_str)
|
33
|
+
return meta.find_undeclared_variables(ast)
|
34
|
+
|
35
|
+
@staticmethod
|
36
|
+
def _find_file_keys(scenario: "Scenario") -> list:
|
37
|
+
"""We need to find all the keys in the scenario that refer to FileStore objects.
|
38
|
+
These will be used to append to the prompt a list of files that are part of the scenario.
|
39
|
+
|
40
|
+
>>> from edsl import Scenario
|
41
|
+
>>> from edsl.scenarios.FileStore import FileStore
|
42
|
+
>>> import tempfile
|
43
|
+
>>> with tempfile.NamedTemporaryFile() as f:
|
44
|
+
... _ = f.write(b"Hello, world!")
|
45
|
+
... _ = f.seek(0)
|
46
|
+
... fs = FileStore(f.name)
|
47
|
+
... scenario = Scenario({"fs_file": fs, 'a': 1})
|
48
|
+
... QuestionTemplateReplacementsBuilder._find_file_keys(scenario)
|
49
|
+
['fs_file']
|
50
|
+
"""
|
51
|
+
from edsl.scenarios.FileStore import FileStore
|
52
|
+
|
53
|
+
file_entries = []
|
54
|
+
for key, value in scenario.items():
|
55
|
+
if isinstance(value, FileStore):
|
56
|
+
file_entries.append(key)
|
57
|
+
return file_entries
|
58
|
+
|
59
|
+
@staticmethod
|
60
|
+
def _extract_file_keys_from_question_text(
|
61
|
+
question_text: str, scenario_file_keys: list
|
62
|
+
) -> list:
|
63
|
+
"""
|
64
|
+
Extracts the file keys from a question text.
|
65
|
+
|
66
|
+
>>> from edsl import Scenario
|
67
|
+
>>> from edsl.scenarios.FileStore import FileStore
|
68
|
+
>>> import tempfile
|
69
|
+
>>> with tempfile.NamedTemporaryFile() as f:
|
70
|
+
... _ = f.write(b"Hello, world!")
|
71
|
+
... _ = f.seek(0)
|
72
|
+
... fs = FileStore(f.name)
|
73
|
+
... scenario = Scenario({"fs_file": fs, 'a': 1})
|
74
|
+
... QuestionTemplateReplacementsBuilder._extract_file_keys_from_question_text("{{ fs_file }}", ['fs_file'])
|
75
|
+
['fs_file']
|
76
|
+
"""
|
77
|
+
variables = QuestionTemplateReplacementsBuilder.get_jinja2_variables(
|
78
|
+
question_text
|
79
|
+
)
|
80
|
+
question_file_keys = []
|
81
|
+
for var in variables:
|
82
|
+
if var in scenario_file_keys:
|
83
|
+
question_file_keys.append(var)
|
84
|
+
return question_file_keys
|
85
|
+
|
86
|
+
def _scenario_replacements(self) -> dict[str, Any]:
|
87
|
+
# File references dictionary
|
88
|
+
file_refs = {key: f"<see file {key}>" for key in self.scenario_file_keys()}
|
89
|
+
|
90
|
+
# Scenario items excluding file keys
|
91
|
+
scenario_items = {
|
92
|
+
k: v
|
93
|
+
for k, v in self.prompt_constructor.scenario.items()
|
94
|
+
if k not in self.scenario_file_keys()
|
95
|
+
}
|
96
|
+
return {**file_refs, **scenario_items}
|
97
|
+
|
98
|
+
@staticmethod
|
99
|
+
def _question_data_replacements(
|
100
|
+
question: dict, question_data: dict
|
101
|
+
) -> dict[str, Any]:
|
102
|
+
"""Builds a dictionary of replacement values for rendering a prompt by combining multiple data sources.
|
103
|
+
|
104
|
+
>>> from edsl import QuestionMultipleChoice
|
105
|
+
>>> q = QuestionMultipleChoice(question_text="Do you like school?", question_name = "q0", question_options = ["yes", "no"])
|
106
|
+
>>> QuestionTemplateReplacementsBuilder._question_data_replacements(q, q.data)
|
107
|
+
{'use_code': False, 'include_comment': True, 'question_name': 'q0', 'question_text': 'Do you like school?', 'question_options': ['yes', 'no']}
|
108
|
+
|
109
|
+
"""
|
110
|
+
question_settings = {
|
111
|
+
"use_code": getattr(question, "_use_code", True),
|
112
|
+
"include_comment": getattr(question, "_include_comment", False),
|
113
|
+
}
|
114
|
+
return {**question_settings, **question_data}
|
115
|
+
|
116
|
+
def build_replacement_dict(self, question_data: dict) -> dict[str, Any]:
|
117
|
+
"""Builds a dictionary of replacement values for rendering a prompt by combining multiple data sources."""
|
118
|
+
rpl = {}
|
119
|
+
rpl["scenario"] = self._scenario_replacements()
|
120
|
+
rpl["question"] = self._question_data_replacements(
|
121
|
+
self.prompt_constructor.question, question_data
|
122
|
+
)
|
123
|
+
rpl["prior_answers"] = self.prompt_constructor.prior_answers_dict()
|
124
|
+
rpl["agent"] = {"agent": self.prompt_constructor.agent}
|
125
|
+
|
126
|
+
# Combine all dictionaries using dict.update() for clarity
|
127
|
+
replacement_dict = {}
|
128
|
+
for r in rpl.values():
|
129
|
+
replacement_dict.update(r)
|
130
|
+
|
131
|
+
return replacement_dict
|
132
|
+
|
133
|
+
|
134
|
+
if __name__ == "__main__":
|
135
|
+
import doctest
|
136
|
+
|
137
|
+
doctest.testmod()
|
edsl/agents/__init__.py
CHANGED
edsl/agents/prompt_helpers.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
import enum
|
2
2
|
from typing import Dict, Optional
|
3
3
|
from collections import UserList
|
4
|
-
from edsl.prompts import Prompt
|
4
|
+
from edsl.prompts.Prompt import Prompt
|
5
5
|
|
6
6
|
|
7
7
|
class PromptComponent(enum.Enum):
|
@@ -12,14 +12,14 @@ class PromptComponent(enum.Enum):
|
|
12
12
|
|
13
13
|
|
14
14
|
class PromptList(UserList):
|
15
|
-
separator = Prompt("
|
15
|
+
separator = Prompt("")
|
16
16
|
|
17
17
|
def reduce(self):
|
18
18
|
"""Reduce the list of prompts to a single prompt.
|
19
19
|
|
20
20
|
>>> p = PromptList([Prompt("You are a happy-go lucky agent."), Prompt("You are an agent with the following persona: {'age': 22, 'hair': 'brown', 'height': 5.5}")])
|
21
21
|
>>> p.reduce()
|
22
|
-
Prompt(text=\"""You are a happy-go lucky agent.
|
22
|
+
Prompt(text=\"""You are a happy-go lucky agent.You are an agent with the following persona: {'age': 22, 'hair': 'brown', 'height': 5.5}\""")
|
23
23
|
|
24
24
|
"""
|
25
25
|
p = self[0]
|
edsl/config.py
CHANGED
@@ -1,12 +1,16 @@
|
|
1
1
|
"""This module provides a Config class that loads environment variables from a .env file and sets them as class attributes."""
|
2
2
|
|
3
3
|
import os
|
4
|
+
import platformdirs
|
4
5
|
from dotenv import load_dotenv, find_dotenv
|
5
|
-
from edsl.exceptions import (
|
6
|
+
from edsl.exceptions.configuration import (
|
6
7
|
InvalidEnvironmentVariableError,
|
7
8
|
MissingEnvironmentVariableError,
|
8
9
|
)
|
9
10
|
|
11
|
+
cache_dir = platformdirs.user_cache_dir("edsl")
|
12
|
+
os.makedirs(cache_dir, exist_ok=True)
|
13
|
+
|
10
14
|
# valid values for EDSL_RUN_MODE
|
11
15
|
EDSL_RUN_MODES = [
|
12
16
|
"development",
|
@@ -34,7 +38,8 @@ CONFIG_MAP = {
|
|
34
38
|
"info": "This config var determines the maximum number of seconds to wait before retrying a failed API call.",
|
35
39
|
},
|
36
40
|
"EDSL_DATABASE_PATH": {
|
37
|
-
"default": f"sqlite:///{os.path.join(os.getcwd(), '.edsl_cache/data.db')}",
|
41
|
+
# "default": f"sqlite:///{os.path.join(os.getcwd(), '.edsl_cache/data.db')}",
|
42
|
+
"default": f"sqlite:///{os.path.join(platformdirs.user_cache_dir('edsl'), 'lm_model_calls.db')}",
|
38
43
|
"info": "This config var determines the path to the cache file.",
|
39
44
|
},
|
40
45
|
"EDSL_DEFAULT_MODEL": {
|
@@ -69,6 +74,10 @@ CONFIG_MAP = {
|
|
69
74
|
"default": "False",
|
70
75
|
"info": "This config var determines whether to open the exception report URL in the browser",
|
71
76
|
},
|
77
|
+
"EDSL_REMOTE_TOKEN_BUCKET_URL": {
|
78
|
+
"default": "None",
|
79
|
+
"info": "This config var holds the URL of the remote token bucket server.",
|
80
|
+
},
|
72
81
|
}
|
73
82
|
|
74
83
|
|
@@ -81,6 +90,9 @@ class Config:
|
|
81
90
|
self._load_dotenv()
|
82
91
|
self._set_env_vars()
|
83
92
|
|
93
|
+
def show_path_to_dot_env(self):
|
94
|
+
print(find_dotenv(usecwd=True))
|
95
|
+
|
84
96
|
def _set_run_mode(self) -> None:
|
85
97
|
"""
|
86
98
|
Sets EDSL_RUN_MODE as a class attribute.
|
@@ -144,6 +156,14 @@ class Config:
|
|
144
156
|
raise MissingEnvironmentVariableError(f"{env_var} is not set. {info}")
|
145
157
|
return self.__dict__.get(env_var)
|
146
158
|
|
159
|
+
def __iter__(self):
|
160
|
+
"""Iterate over the environment variables."""
|
161
|
+
return iter(self.__dict__)
|
162
|
+
|
163
|
+
def items(self):
|
164
|
+
"""Iterate over the environment variables and their values."""
|
165
|
+
return self.__dict__.items()
|
166
|
+
|
147
167
|
def show(self) -> str:
|
148
168
|
"""Print the currently set environment vars."""
|
149
169
|
max_env_var_length = max(len(env_var) for env_var in self.__dict__)
|
edsl/conversation/car_buying.py
CHANGED
@@ -29,7 +29,8 @@ a3 = Agent(
|
|
29
29
|
c1 = Conversation(agent_list=AgentList([a1, a3, a2]), max_turns=5, verbose=True)
|
30
30
|
c2 = Conversation(agent_list=AgentList([a1, a2]), max_turns=5, verbose=True)
|
31
31
|
|
32
|
-
c = Cache.load("car_talk.json.gz")
|
32
|
+
# c = Cache.load("car_talk.json.gz")
|
33
|
+
c = Cache()
|
33
34
|
# breakpoint()
|
34
35
|
combo = ConversationList([c1, c2], cache=c)
|
35
36
|
combo.run()
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class CoopFunctionsMixin:
|
2
|
+
def better_names(self, existing_names):
|
3
|
+
from edsl import QuestionList, Scenario
|
4
|
+
|
5
|
+
s = Scenario({"existing_names": existing_names})
|
6
|
+
q = QuestionList(
|
7
|
+
question_text="""The following colum names are already in use: {{ existing_names }}
|
8
|
+
Please provide new names for the columns.
|
9
|
+
They should be short, one or two words, and unique. They should be valid Python idenifiers.
|
10
|
+
No spaces - use underscores instead.
|
11
|
+
""",
|
12
|
+
question_name="better_names",
|
13
|
+
)
|
14
|
+
results = q.by(s).run(verbose=False)
|
15
|
+
return results.select("answer.better_names").first()
|
@@ -0,0 +1,125 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
import os
|
3
|
+
import platformdirs
|
4
|
+
|
5
|
+
|
6
|
+
import sys
|
7
|
+
import select
|
8
|
+
|
9
|
+
|
10
|
+
def get_input_with_timeout(prompt, timeout=5, default="y"):
|
11
|
+
print(prompt, end="", flush=True)
|
12
|
+
ready, _, _ = select.select([sys.stdin], [], [], timeout)
|
13
|
+
if ready:
|
14
|
+
return sys.stdin.readline().strip()
|
15
|
+
print(f"\nNo input received within {timeout} seconds. Using default: {default}")
|
16
|
+
return default
|
17
|
+
|
18
|
+
|
19
|
+
class ExpectedParrotKeyHandler:
|
20
|
+
asked_to_store_file_name = "asked_to_store.txt"
|
21
|
+
ep_key_file_name = "ep_api_key.txt"
|
22
|
+
application_name = "edsl"
|
23
|
+
|
24
|
+
@property
|
25
|
+
def config_dir(self):
|
26
|
+
return platformdirs.user_config_dir(self.application_name)
|
27
|
+
|
28
|
+
def _ep_key_file_exists(self) -> bool:
|
29
|
+
"""Check if the Expected Parrot key file exists."""
|
30
|
+
return Path(self.config_dir).joinpath(self.ep_key_file_name).exists()
|
31
|
+
|
32
|
+
def ok_to_ask_to_store(self):
|
33
|
+
"""Check if it's okay to ask the user to store the key."""
|
34
|
+
from edsl.config import CONFIG
|
35
|
+
|
36
|
+
if CONFIG.get("EDSL_RUN_MODE") != "production":
|
37
|
+
return False
|
38
|
+
|
39
|
+
return (
|
40
|
+
not Path(self.config_dir).joinpath(self.asked_to_store_file_name).exists()
|
41
|
+
)
|
42
|
+
|
43
|
+
def reset_asked_to_store(self):
|
44
|
+
"""Reset the flag that indicates whether the user has been asked to store the key."""
|
45
|
+
asked_to_store_path = Path(self.config_dir).joinpath(
|
46
|
+
self.asked_to_store_file_name
|
47
|
+
)
|
48
|
+
if asked_to_store_path.exists():
|
49
|
+
os.remove(asked_to_store_path)
|
50
|
+
print(
|
51
|
+
"Deleted the file that indicates whether the user has been asked to store the key."
|
52
|
+
)
|
53
|
+
|
54
|
+
def ask_to_store(self, api_key) -> bool:
|
55
|
+
"""Ask the user if they want to store the Expected Parrot key. If they say "yes", store it."""
|
56
|
+
if self.ok_to_ask_to_store():
|
57
|
+
# can_we_store = get_input_with_timeout(
|
58
|
+
# "Would you like to store your Expected Parrot key for future use? (y/n): ",
|
59
|
+
# timeout=5,
|
60
|
+
# default="y",
|
61
|
+
# )
|
62
|
+
can_we_store = "y"
|
63
|
+
if can_we_store.lower() == "y":
|
64
|
+
Path(self.config_dir).mkdir(parents=True, exist_ok=True)
|
65
|
+
self.store_ep_api_key(api_key)
|
66
|
+
# print("Stored Expected Parrot API key at ", self.config_dir)
|
67
|
+
return True
|
68
|
+
else:
|
69
|
+
Path(self.config_dir).mkdir(parents=True, exist_ok=True)
|
70
|
+
with open(
|
71
|
+
Path(self.config_dir).joinpath(self.asked_to_store_file_name), "w"
|
72
|
+
) as f:
|
73
|
+
f.write("Yes")
|
74
|
+
return False
|
75
|
+
|
76
|
+
def get_ep_api_key(self):
|
77
|
+
# check if the key is stored in the config_dir
|
78
|
+
api_key = None
|
79
|
+
api_key_from_cache = None
|
80
|
+
api_key_from_os = None
|
81
|
+
|
82
|
+
if self._ep_key_file_exists():
|
83
|
+
with open(Path(self.config_dir).joinpath(self.ep_key_file_name), "r") as f:
|
84
|
+
api_key_from_cache = f.read().strip()
|
85
|
+
|
86
|
+
api_key_from_os = os.getenv("EXPECTED_PARROT_API_KEY")
|
87
|
+
|
88
|
+
if api_key_from_os and api_key_from_cache:
|
89
|
+
if api_key_from_os != api_key_from_cache:
|
90
|
+
import warnings
|
91
|
+
|
92
|
+
warnings.warn(
|
93
|
+
"WARNING: The Expected Parrot API key from the environment variable "
|
94
|
+
"differs from the one stored in the config directory. Using the one "
|
95
|
+
"from the environment variable."
|
96
|
+
)
|
97
|
+
api_key = api_key_from_os
|
98
|
+
|
99
|
+
if api_key_from_os and not api_key_from_cache:
|
100
|
+
api_key = api_key_from_os
|
101
|
+
|
102
|
+
if not api_key_from_os and api_key_from_cache:
|
103
|
+
api_key = api_key_from_cache
|
104
|
+
|
105
|
+
if api_key is not None:
|
106
|
+
_ = self.ask_to_store(api_key)
|
107
|
+
return api_key
|
108
|
+
|
109
|
+
def delete_ep_api_key(self):
|
110
|
+
key_path = Path(self.config_dir) / self.ep_key_file_name
|
111
|
+
if key_path.exists():
|
112
|
+
os.remove(key_path)
|
113
|
+
print("Deleted Expected Parrot API key at ", key_path)
|
114
|
+
|
115
|
+
def store_ep_api_key(self, api_key):
|
116
|
+
# Create the directory if it doesn't exist
|
117
|
+
os.makedirs(self.config_dir, exist_ok=True)
|
118
|
+
|
119
|
+
# Create the path for the key file
|
120
|
+
key_path = Path(self.config_dir) / self.ep_key_file_name
|
121
|
+
|
122
|
+
# Save the key
|
123
|
+
with open(key_path, "w") as f:
|
124
|
+
f.write(api_key)
|
125
|
+
# print("Stored Expected Parrot API key at ", key_path)
|