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.
Files changed (194) hide show
  1. edsl/Base.py +169 -116
  2. edsl/__init__.py +14 -6
  3. edsl/__version__.py +1 -1
  4. edsl/agents/Agent.py +358 -146
  5. edsl/agents/AgentList.py +211 -73
  6. edsl/agents/Invigilator.py +88 -36
  7. edsl/agents/InvigilatorBase.py +59 -70
  8. edsl/agents/PromptConstructor.py +117 -219
  9. edsl/agents/QuestionInstructionPromptBuilder.py +128 -0
  10. edsl/agents/QuestionOptionProcessor.py +172 -0
  11. edsl/agents/QuestionTemplateReplacementsBuilder.py +137 -0
  12. edsl/agents/__init__.py +0 -1
  13. edsl/agents/prompt_helpers.py +3 -3
  14. edsl/config.py +22 -2
  15. edsl/conversation/car_buying.py +2 -1
  16. edsl/coop/CoopFunctionsMixin.py +15 -0
  17. edsl/coop/ExpectedParrotKeyHandler.py +125 -0
  18. edsl/coop/PriceFetcher.py +1 -1
  19. edsl/coop/coop.py +104 -42
  20. edsl/coop/utils.py +14 -14
  21. edsl/data/Cache.py +21 -14
  22. edsl/data/CacheEntry.py +12 -15
  23. edsl/data/CacheHandler.py +33 -12
  24. edsl/data/__init__.py +4 -3
  25. edsl/data_transfer_models.py +2 -1
  26. edsl/enums.py +20 -0
  27. edsl/exceptions/__init__.py +50 -50
  28. edsl/exceptions/agents.py +12 -0
  29. edsl/exceptions/inference_services.py +5 -0
  30. edsl/exceptions/questions.py +24 -6
  31. edsl/exceptions/scenarios.py +7 -0
  32. edsl/inference_services/AnthropicService.py +0 -3
  33. edsl/inference_services/AvailableModelCacheHandler.py +184 -0
  34. edsl/inference_services/AvailableModelFetcher.py +209 -0
  35. edsl/inference_services/AwsBedrock.py +0 -2
  36. edsl/inference_services/AzureAI.py +0 -2
  37. edsl/inference_services/GoogleService.py +2 -11
  38. edsl/inference_services/InferenceServiceABC.py +18 -85
  39. edsl/inference_services/InferenceServicesCollection.py +105 -80
  40. edsl/inference_services/MistralAIService.py +0 -3
  41. edsl/inference_services/OpenAIService.py +1 -4
  42. edsl/inference_services/PerplexityService.py +0 -3
  43. edsl/inference_services/ServiceAvailability.py +135 -0
  44. edsl/inference_services/TestService.py +11 -8
  45. edsl/inference_services/data_structures.py +62 -0
  46. edsl/jobs/AnswerQuestionFunctionConstructor.py +188 -0
  47. edsl/jobs/Answers.py +1 -14
  48. edsl/jobs/FetchInvigilator.py +40 -0
  49. edsl/jobs/InterviewTaskManager.py +98 -0
  50. edsl/jobs/InterviewsConstructor.py +48 -0
  51. edsl/jobs/Jobs.py +102 -243
  52. edsl/jobs/JobsChecks.py +35 -10
  53. edsl/jobs/JobsComponentConstructor.py +189 -0
  54. edsl/jobs/JobsPrompts.py +5 -3
  55. edsl/jobs/JobsRemoteInferenceHandler.py +128 -80
  56. edsl/jobs/JobsRemoteInferenceLogger.py +239 -0
  57. edsl/jobs/RequestTokenEstimator.py +30 -0
  58. edsl/jobs/buckets/BucketCollection.py +44 -3
  59. edsl/jobs/buckets/TokenBucket.py +53 -21
  60. edsl/jobs/buckets/TokenBucketAPI.py +211 -0
  61. edsl/jobs/buckets/TokenBucketClient.py +191 -0
  62. edsl/jobs/decorators.py +35 -0
  63. edsl/jobs/interviews/Interview.py +77 -380
  64. edsl/jobs/jobs_status_enums.py +9 -0
  65. edsl/jobs/loggers/HTMLTableJobLogger.py +304 -0
  66. edsl/jobs/runners/JobsRunnerAsyncio.py +4 -49
  67. edsl/jobs/tasks/QuestionTaskCreator.py +21 -19
  68. edsl/jobs/tasks/TaskHistory.py +14 -15
  69. edsl/jobs/tasks/task_status_enum.py +0 -2
  70. edsl/language_models/ComputeCost.py +63 -0
  71. edsl/language_models/LanguageModel.py +137 -234
  72. edsl/language_models/ModelList.py +11 -13
  73. edsl/language_models/PriceManager.py +127 -0
  74. edsl/language_models/RawResponseHandler.py +106 -0
  75. edsl/language_models/ServiceDataSources.py +0 -0
  76. edsl/language_models/__init__.py +0 -1
  77. edsl/language_models/key_management/KeyLookup.py +63 -0
  78. edsl/language_models/key_management/KeyLookupBuilder.py +273 -0
  79. edsl/language_models/key_management/KeyLookupCollection.py +38 -0
  80. edsl/language_models/key_management/__init__.py +0 -0
  81. edsl/language_models/key_management/models.py +131 -0
  82. edsl/language_models/registry.py +49 -59
  83. edsl/language_models/repair.py +2 -2
  84. edsl/language_models/utilities.py +5 -4
  85. edsl/notebooks/Notebook.py +19 -14
  86. edsl/notebooks/NotebookToLaTeX.py +142 -0
  87. edsl/prompts/Prompt.py +29 -39
  88. edsl/questions/AnswerValidatorMixin.py +47 -2
  89. edsl/questions/ExceptionExplainer.py +77 -0
  90. edsl/questions/HTMLQuestion.py +103 -0
  91. edsl/questions/LoopProcessor.py +149 -0
  92. edsl/questions/QuestionBase.py +37 -192
  93. edsl/questions/QuestionBaseGenMixin.py +52 -48
  94. edsl/questions/QuestionBasePromptsMixin.py +7 -3
  95. edsl/questions/QuestionCheckBox.py +1 -1
  96. edsl/questions/QuestionExtract.py +1 -1
  97. edsl/questions/QuestionFreeText.py +1 -2
  98. edsl/questions/QuestionList.py +3 -5
  99. edsl/questions/QuestionMatrix.py +265 -0
  100. edsl/questions/QuestionMultipleChoice.py +66 -22
  101. edsl/questions/QuestionNumerical.py +1 -3
  102. edsl/questions/QuestionRank.py +6 -16
  103. edsl/questions/ResponseValidatorABC.py +37 -11
  104. edsl/questions/ResponseValidatorFactory.py +28 -0
  105. edsl/questions/SimpleAskMixin.py +4 -3
  106. edsl/questions/__init__.py +1 -0
  107. edsl/questions/derived/QuestionLinearScale.py +6 -3
  108. edsl/questions/derived/QuestionTopK.py +1 -1
  109. edsl/questions/descriptors.py +17 -3
  110. edsl/questions/question_registry.py +1 -1
  111. edsl/questions/templates/matrix/__init__.py +1 -0
  112. edsl/questions/templates/matrix/answering_instructions.jinja +5 -0
  113. edsl/questions/templates/matrix/question_presentation.jinja +20 -0
  114. edsl/results/CSSParameterizer.py +1 -1
  115. edsl/results/Dataset.py +170 -7
  116. edsl/results/DatasetExportMixin.py +224 -302
  117. edsl/results/DatasetTree.py +28 -8
  118. edsl/results/MarkdownToDocx.py +122 -0
  119. edsl/results/MarkdownToPDF.py +111 -0
  120. edsl/results/Result.py +192 -206
  121. edsl/results/Results.py +120 -113
  122. edsl/results/ResultsExportMixin.py +2 -0
  123. edsl/results/Selector.py +23 -13
  124. edsl/results/TableDisplay.py +98 -171
  125. edsl/results/TextEditor.py +50 -0
  126. edsl/results/__init__.py +1 -1
  127. edsl/results/smart_objects.py +96 -0
  128. edsl/results/table_data_class.py +12 -0
  129. edsl/results/table_renderers.py +118 -0
  130. edsl/scenarios/ConstructDownloadLink.py +109 -0
  131. edsl/scenarios/DirectoryScanner.py +96 -0
  132. edsl/scenarios/DocumentChunker.py +102 -0
  133. edsl/scenarios/DocxScenario.py +16 -0
  134. edsl/scenarios/FileStore.py +118 -239
  135. edsl/scenarios/PdfExtractor.py +40 -0
  136. edsl/scenarios/Scenario.py +90 -193
  137. edsl/scenarios/ScenarioHtmlMixin.py +4 -3
  138. edsl/scenarios/ScenarioJoin.py +10 -6
  139. edsl/scenarios/ScenarioList.py +383 -240
  140. edsl/scenarios/ScenarioListExportMixin.py +0 -7
  141. edsl/scenarios/ScenarioListPdfMixin.py +15 -37
  142. edsl/scenarios/ScenarioSelector.py +156 -0
  143. edsl/scenarios/__init__.py +1 -2
  144. edsl/scenarios/file_methods.py +85 -0
  145. edsl/scenarios/handlers/__init__.py +13 -0
  146. edsl/scenarios/handlers/csv.py +38 -0
  147. edsl/scenarios/handlers/docx.py +76 -0
  148. edsl/scenarios/handlers/html.py +37 -0
  149. edsl/scenarios/handlers/json.py +111 -0
  150. edsl/scenarios/handlers/latex.py +5 -0
  151. edsl/scenarios/handlers/md.py +51 -0
  152. edsl/scenarios/handlers/pdf.py +68 -0
  153. edsl/scenarios/handlers/png.py +39 -0
  154. edsl/scenarios/handlers/pptx.py +105 -0
  155. edsl/scenarios/handlers/py.py +294 -0
  156. edsl/scenarios/handlers/sql.py +313 -0
  157. edsl/scenarios/handlers/sqlite.py +149 -0
  158. edsl/scenarios/handlers/txt.py +33 -0
  159. edsl/study/ObjectEntry.py +1 -1
  160. edsl/study/SnapShot.py +1 -1
  161. edsl/study/Study.py +5 -12
  162. edsl/surveys/ConstructDAG.py +92 -0
  163. edsl/surveys/EditSurvey.py +221 -0
  164. edsl/surveys/InstructionHandler.py +100 -0
  165. edsl/surveys/MemoryManagement.py +72 -0
  166. edsl/surveys/Rule.py +5 -4
  167. edsl/surveys/RuleCollection.py +25 -27
  168. edsl/surveys/RuleManager.py +172 -0
  169. edsl/surveys/Simulator.py +75 -0
  170. edsl/surveys/Survey.py +199 -771
  171. edsl/surveys/SurveyCSS.py +20 -8
  172. edsl/surveys/{SurveyFlowVisualizationMixin.py → SurveyFlowVisualization.py} +11 -9
  173. edsl/surveys/SurveyToApp.py +141 -0
  174. edsl/surveys/__init__.py +4 -2
  175. edsl/surveys/descriptors.py +6 -2
  176. edsl/surveys/instructions/ChangeInstruction.py +1 -2
  177. edsl/surveys/instructions/Instruction.py +4 -13
  178. edsl/surveys/instructions/InstructionCollection.py +11 -6
  179. edsl/templates/error_reporting/interview_details.html +1 -1
  180. edsl/templates/error_reporting/report.html +1 -1
  181. edsl/tools/plotting.py +1 -1
  182. edsl/utilities/PrettyList.py +56 -0
  183. edsl/utilities/is_notebook.py +18 -0
  184. edsl/utilities/is_valid_variable_name.py +11 -0
  185. edsl/utilities/remove_edsl_version.py +24 -0
  186. edsl/utilities/utilities.py +35 -23
  187. {edsl-0.1.39.dev1.dist-info → edsl-0.1.39.dev2.dist-info}/METADATA +12 -10
  188. edsl-0.1.39.dev2.dist-info/RECORD +352 -0
  189. edsl/language_models/KeyLookup.py +0 -30
  190. edsl/language_models/unused/ReplicateBase.py +0 -83
  191. edsl/results/ResultsDBMixin.py +0 -238
  192. edsl-0.1.39.dev1.dist-info/RECORD +0 -277
  193. {edsl-0.1.39.dev1.dist-info → edsl-0.1.39.dev2.dist-info}/LICENSE +0 -0
  194. {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
@@ -1,3 +1,2 @@
1
1
  from edsl.agents.Agent import Agent
2
2
  from edsl.agents.AgentList import AgentList
3
- from edsl.agents.InvigilatorBase import InvigilatorBase
@@ -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. You are an agent with the following persona: {'age': 22, 'hair': 'brown', 'height': 5.5}\""")
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__)
@@ -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)