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
@@ -2,7 +2,7 @@
2
2
 
3
3
  import re
4
4
  from typing import Any, Type, Union
5
- from edsl.exceptions import (
5
+ from edsl.exceptions.questions import (
6
6
  QuestionAnswerValidationError,
7
7
  )
8
8
 
@@ -213,7 +213,12 @@ class AnswerValidatorMixin:
213
213
  - is not less than `min_value`
214
214
  - is not greater than `max_value`
215
215
  """
216
- value = float(answer.get("answer"))
216
+ try:
217
+ value = float(answer.get("answer"))
218
+ except ValueError:
219
+ raise QuestionAnswerValidationError(
220
+ f"Answer must real number or convertible to a real number (got {answer.get('answer')})."
221
+ )
217
222
  if self.min_value is not None and value < self.min_value:
218
223
  raise QuestionAnswerValidationError(
219
224
  f"Value {value} is less than {self.min_value}"
@@ -279,6 +284,46 @@ class AnswerValidatorMixin:
279
284
  f"Rank answer {value}, but exactly {self.num_selections} selections required."
280
285
  )
281
286
 
287
+ def _validate_answer_matrix(self, answer: dict[str, Any]) -> None:
288
+ """Validate QuestionMatrix-specific answer.
289
+
290
+ Check that answer["answer"]:
291
+ - is a dictionary
292
+ - has all required question_items as keys
293
+ - has values that are valid options from question_options
294
+ - has the correct number of responses (one per item)
295
+ """
296
+ value = answer.get("answer")
297
+
298
+ # Check that answer is a dictionary
299
+ if not isinstance(value, dict):
300
+ raise QuestionAnswerValidationError(
301
+ f"Matrix answer must be a dictionary mapping items to responses (got {value})"
302
+ )
303
+
304
+ # Check that all required items are present
305
+ required_items = set(self.question_items)
306
+ provided_items = set(value.keys())
307
+
308
+ if missing_items := (required_items - provided_items):
309
+ raise QuestionAnswerValidationError(
310
+ f"Missing responses for items: {missing_items}"
311
+ )
312
+
313
+ if extra_items := (provided_items - required_items):
314
+ raise QuestionAnswerValidationError(
315
+ f"Unexpected responses for items: {extra_items}"
316
+ )
317
+
318
+ # Check that all responses are valid options
319
+ valid_options = set(self.question_options)
320
+ for item, response in value.items():
321
+ if response not in valid_options:
322
+ raise QuestionAnswerValidationError(
323
+ f"Invalid response '{response}' for item '{item}'. "
324
+ f"Must be one of: {valid_options}"
325
+ )
326
+
282
327
 
283
328
  if __name__ == "__main__":
284
329
  pass
@@ -0,0 +1,77 @@
1
+ from pydantic import ValidationError
2
+ from typing import Union
3
+
4
+
5
+ class ExceptionExplainer:
6
+ """
7
+ A class that converts validation errors into human-readable explanations,
8
+ specifically for Language Model responses.
9
+ """
10
+
11
+ def __init__(self, error: Union[ValidationError, Exception], model_response: str):
12
+ """
13
+ Initialize the explainer with the error and model response.
14
+
15
+ Args:
16
+ error: The validation error that occurred
17
+ model_response: The raw response from the Language Model
18
+ """
19
+ self.error = error
20
+ self.model_response = model_response
21
+
22
+ def explain(self) -> str:
23
+ """
24
+ Generate a human-readable explanation of why the model's response failed validation.
25
+
26
+ Returns:
27
+ A user-friendly explanation of why the model's response was invalid
28
+ """
29
+ self.error = self.error.pydantic_error
30
+ return self._explain_validation_error()
31
+
32
+ # Fallback for unknown errors
33
+ return self._create_generic_explanation()
34
+
35
+ def _explain_validation_error(self) -> str:
36
+ """Handle Pydantic ValidationError specifically."""
37
+ error_dict = self.error.errors()
38
+ explanations = []
39
+
40
+ context = f'The AI model returned "{self.model_response}", but this was invalid for the question you asked and the constraints you provided.\n'
41
+ explanations.append(context)
42
+ explanations.append("Reason(s) invalidated:")
43
+ for e in error_dict:
44
+ msg = e.get("msg", "Unknown error")
45
+ explanations.append(f"- {msg}")
46
+
47
+ main_message = "\n".join(explanations)
48
+ return f"{main_message}\n\n{self._get_suggestion()}"
49
+
50
+ def _create_generic_explanation(self) -> str:
51
+ """Create a generic explanation for non-ValidationError exceptions."""
52
+ return (
53
+ f'The AI model returned "{self.model_response}", but this response was invalid. '
54
+ f"Error: {str(self.error)}"
55
+ )
56
+
57
+ def _get_suggestion(self) -> str:
58
+ """Get a suggestion for handling the error."""
59
+ return (
60
+ "EDSL Advice:\n"
61
+ "- Look at the Model comments - often the model will provide a hint about what went wrong.\n"
62
+ "- If the model's response doesn't make sense, try rephrasing your question.\n"
63
+ "- Try using 'use_code' parameter of a MultipleChoice.\n"
64
+ "- A QuestionFreeText will almost always validate.\n"
65
+ "- Try setting the 'permissive' = True parameter in the Question constructor."
66
+ )
67
+
68
+
69
+ # Example usage:
70
+ if __name__ == "__main__":
71
+ try:
72
+ # Your validation code here
73
+ raise ValidationError.parse_obj({"answer": "120"})
74
+ except ValidationError as e:
75
+ explainer = ExceptionExplainer(e, "120")
76
+ explanation = explainer.explain()
77
+ print(explanation)
@@ -0,0 +1,103 @@
1
+ from typing import Optional
2
+ from edsl.prompts.Prompt import Prompt
3
+
4
+
5
+ class HTMLQuestion:
6
+ def __init__(self, question):
7
+ self.question = question
8
+
9
+ def html(
10
+ self,
11
+ scenario: Optional[dict] = None,
12
+ agent: Optional[dict] = {},
13
+ answers: Optional[dict] = None,
14
+ include_question_name: bool = False,
15
+ height: Optional[int] = None,
16
+ width: Optional[int] = None,
17
+ iframe=False,
18
+ ):
19
+ """Return the question in HTML format."""
20
+ from jinja2 import Template
21
+
22
+ if scenario is None:
23
+ scenario = {}
24
+
25
+ prior_answers_dict = {}
26
+
27
+ if isinstance(answers, dict):
28
+ for key, value in answers.items():
29
+ if not key.endswith("_comment") and not key.endswith(
30
+ "_generated_tokens"
31
+ ):
32
+ prior_answers_dict[key] = {"answer": value}
33
+
34
+ base_template = """
35
+ <div id="{{ question_name }}" class="survey_question" data-type="{{ question_type }}">
36
+ {% if include_question_name %}
37
+ <p>question_name: {{ question_name }}</p>
38
+ {% endif %}
39
+ <p class="question_text">{{ question_text }}</p>
40
+ {{ question_content }}
41
+ </div>
42
+ """
43
+ if not hasattr(self.question, "question_type"):
44
+ self.question.question_type = "unknown"
45
+
46
+ if hasattr(self.question, "question_html_content"):
47
+ question_content = self.question.question_html_content
48
+ else:
49
+ question_content = Template("")
50
+
51
+ base_template = Template(base_template)
52
+
53
+ context = {
54
+ "scenario": scenario,
55
+ "agent": agent,
56
+ } | prior_answers_dict
57
+
58
+ # Render the question text
59
+ try:
60
+ question_text = Template(self.question.question_text).render(context)
61
+ except Exception as e:
62
+ print(
63
+ f"Error rendering question: question_text = {self.question.question_text}, error = {e}"
64
+ )
65
+ question_text = self.question.question_text
66
+
67
+ try:
68
+ question_content = Template(question_content).render(context)
69
+ except Exception as e:
70
+ print(
71
+ f"Error rendering question: question_content = {question_content}, error = {e}"
72
+ )
73
+ question_content = question_content
74
+
75
+ try:
76
+ params = {
77
+ "question_name": self.question.question_name,
78
+ "question_text": question_text,
79
+ "question_type": self.question.question_type,
80
+ "question_content": question_content,
81
+ "include_question_name": include_question_name,
82
+ }
83
+ except Exception as e:
84
+ raise ValueError(
85
+ f"Error rendering question: params = {params}, error = {e}"
86
+ )
87
+ rendered_html = base_template.render(**params)
88
+
89
+ if iframe:
90
+ import html
91
+ from IPython.display import display, HTML
92
+
93
+ height = height or 200
94
+ width = width or 600
95
+ escaped_output = html.escape(rendered_html)
96
+ # escaped_output = rendered_html
97
+ iframe = f""""
98
+ <iframe srcdoc="{ escaped_output }" style="width: {width}px; height: {height}px;"></iframe>
99
+ """
100
+ display(HTML(iframe))
101
+ return None
102
+
103
+ return rendered_html
@@ -0,0 +1,149 @@
1
+ from typing import List, Any, Dict, Union
2
+ from jinja2 import Environment
3
+ from edsl.questions.QuestionBase import QuestionBase
4
+ from edsl import ScenarioList
5
+
6
+
7
+ class LoopProcessor:
8
+ def __init__(self, question: QuestionBase):
9
+ self.question = question
10
+ self.env = Environment()
11
+
12
+ def process_templates(self, scenario_list: ScenarioList) -> List[QuestionBase]:
13
+ """Process templates for each scenario and return list of modified questions.
14
+
15
+ Args:
16
+ scenario_list: List of scenarios to process templates against
17
+
18
+ Returns:
19
+ List of QuestionBase objects with rendered templates
20
+ """
21
+ questions = []
22
+ starting_name = self.question.question_name
23
+
24
+ for index, scenario in enumerate(scenario_list):
25
+ question_data = self.question.to_dict().copy()
26
+ processed_data = self._process_data(question_data, scenario)
27
+
28
+ if processed_data["question_name"] == starting_name:
29
+ processed_data["question_name"] += f"_{index}"
30
+
31
+ questions.append(QuestionBase.from_dict(processed_data))
32
+
33
+ return questions
34
+
35
+ def _process_data(
36
+ self, data: Dict[str, Any], scenario: Dict[str, Any]
37
+ ) -> Dict[str, Any]:
38
+ """Process all data fields according to their type.
39
+
40
+ Args:
41
+ data: Dictionary of question data
42
+ scenario: Current scenario to render templates against
43
+
44
+ Returns:
45
+ Processed dictionary with rendered templates
46
+ """
47
+ processed = {}
48
+
49
+ for key, value in [(k, v) for k, v in data.items() if v is not None]:
50
+ processed[key] = self._process_value(key, value, scenario)
51
+
52
+ return processed
53
+
54
+ def _process_value(self, key: str, value: Any, scenario: Dict[str, Any]) -> Any:
55
+ """Process a single value according to its type.
56
+
57
+ Args:
58
+ key: Dictionary key
59
+ value: Value to process
60
+ scenario: Current scenario
61
+
62
+ Returns:
63
+ Processed value
64
+ """
65
+ if key == "question_options" and isinstance(value, str):
66
+ return value
67
+
68
+ if key == "option_labels":
69
+ import json
70
+
71
+ return (
72
+ eval(self._render_template(value, scenario))
73
+ if isinstance(value, str)
74
+ else value
75
+ )
76
+
77
+ if isinstance(value, str):
78
+ return self._render_template(value, scenario)
79
+
80
+ if isinstance(value, list):
81
+ return self._process_list(value, scenario)
82
+
83
+ if isinstance(value, dict):
84
+ return self._process_dict(value, scenario)
85
+
86
+ if isinstance(value, (int, float)):
87
+ return value
88
+
89
+ raise ValueError(f"Unexpected value type: {type(value)} for key '{key}'")
90
+
91
+ def _render_template(self, template: str, scenario: Dict[str, Any]) -> str:
92
+ """Render a single template string.
93
+
94
+ Args:
95
+ template: Template string to render
96
+ scenario: Current scenario
97
+
98
+ Returns:
99
+ Rendered template string
100
+ """
101
+ return self.env.from_string(template).render(scenario)
102
+
103
+ def _process_list(self, items: List[Any], scenario: Dict[str, Any]) -> List[Any]:
104
+ """Process all items in a list.
105
+
106
+ Args:
107
+ items: List of items to process
108
+ scenario: Current scenario
109
+
110
+ Returns:
111
+ List of processed items
112
+ """
113
+ return [
114
+ self._render_template(item, scenario) if isinstance(item, str) else item
115
+ for item in items
116
+ ]
117
+
118
+ def _process_dict(
119
+ self, data: Dict[str, Any], scenario: Dict[str, Any]
120
+ ) -> Dict[str, Any]:
121
+ """Process all keys and values in a dictionary.
122
+
123
+ Args:
124
+ data: Dictionary to process
125
+ scenario: Current scenario
126
+
127
+ Returns:
128
+ Dictionary with processed keys and values
129
+ """
130
+ return {
131
+ (self._render_template(k, scenario) if isinstance(k, str) else k): (
132
+ self._render_template(v, scenario) if isinstance(v, str) else v
133
+ )
134
+ for k, v in data.items()
135
+ }
136
+
137
+
138
+ # Usage example:
139
+ """
140
+ from edsl import QuestionFreeText, ScenarioList
141
+
142
+ question = QuestionFreeText(
143
+ question_text="What are your thoughts on: {{subject}}?",
144
+ question_name="base_{{subject}}"
145
+ )
146
+ processor = TemplateProcessor(question)
147
+ scenarios = ScenarioList.from_list("subject", ["Math", "Economics", "Chemistry"])
148
+ processed_questions = processor.process_templates(scenarios)
149
+ """