edsl 0.1.39__py3-none-any.whl → 0.1.39.dev1__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 +116 -197
- edsl/__init__.py +7 -15
- edsl/__version__.py +1 -1
- edsl/agents/Agent.py +147 -351
- edsl/agents/AgentList.py +73 -211
- edsl/agents/Invigilator.py +50 -101
- edsl/agents/InvigilatorBase.py +70 -62
- edsl/agents/PromptConstructor.py +225 -143
- edsl/agents/__init__.py +1 -0
- edsl/agents/prompt_helpers.py +3 -3
- edsl/auto/AutoStudy.py +5 -18
- edsl/auto/StageBase.py +40 -53
- edsl/auto/StageQuestions.py +1 -2
- edsl/auto/utilities.py +6 -0
- edsl/config.py +2 -22
- edsl/conversation/car_buying.py +1 -2
- edsl/coop/PriceFetcher.py +1 -1
- edsl/coop/coop.py +47 -125
- edsl/coop/utils.py +14 -14
- edsl/data/Cache.py +27 -45
- edsl/data/CacheEntry.py +15 -12
- edsl/data/CacheHandler.py +12 -31
- edsl/data/RemoteCacheSync.py +46 -154
- edsl/data/__init__.py +3 -4
- edsl/data_transfer_models.py +1 -2
- edsl/enums.py +0 -27
- edsl/exceptions/__init__.py +50 -50
- edsl/exceptions/agents.py +0 -12
- edsl/exceptions/questions.py +6 -24
- edsl/exceptions/scenarios.py +0 -7
- edsl/inference_services/AnthropicService.py +19 -38
- edsl/inference_services/AwsBedrock.py +2 -0
- edsl/inference_services/AzureAI.py +2 -0
- edsl/inference_services/GoogleService.py +12 -7
- edsl/inference_services/InferenceServiceABC.py +85 -18
- edsl/inference_services/InferenceServicesCollection.py +79 -120
- edsl/inference_services/MistralAIService.py +3 -0
- edsl/inference_services/OpenAIService.py +35 -47
- edsl/inference_services/PerplexityService.py +3 -0
- edsl/inference_services/TestService.py +10 -11
- edsl/inference_services/TogetherAIService.py +3 -5
- edsl/jobs/Answers.py +14 -1
- edsl/jobs/Jobs.py +431 -356
- edsl/jobs/JobsChecks.py +10 -35
- edsl/jobs/JobsPrompts.py +4 -6
- edsl/jobs/JobsRemoteInferenceHandler.py +133 -205
- edsl/jobs/buckets/BucketCollection.py +3 -44
- edsl/jobs/buckets/TokenBucket.py +21 -53
- edsl/jobs/interviews/Interview.py +408 -143
- edsl/jobs/runners/JobsRunnerAsyncio.py +403 -88
- edsl/jobs/runners/JobsRunnerStatus.py +165 -133
- edsl/jobs/tasks/QuestionTaskCreator.py +19 -21
- edsl/jobs/tasks/TaskHistory.py +18 -38
- edsl/jobs/tasks/task_status_enum.py +2 -0
- edsl/language_models/KeyLookup.py +30 -0
- edsl/language_models/LanguageModel.py +236 -194
- edsl/language_models/ModelList.py +19 -28
- edsl/language_models/__init__.py +2 -1
- edsl/language_models/registry.py +190 -0
- edsl/language_models/repair.py +2 -2
- edsl/language_models/unused/ReplicateBase.py +83 -0
- edsl/language_models/utilities.py +4 -5
- edsl/notebooks/Notebook.py +14 -19
- edsl/prompts/Prompt.py +39 -29
- edsl/questions/{answer_validator_mixin.py → AnswerValidatorMixin.py} +2 -47
- edsl/questions/QuestionBase.py +214 -68
- edsl/questions/{question_base_gen_mixin.py → QuestionBaseGenMixin.py} +50 -57
- edsl/questions/QuestionBasePromptsMixin.py +3 -7
- edsl/questions/QuestionBudget.py +1 -1
- edsl/questions/QuestionCheckBox.py +3 -3
- edsl/questions/QuestionExtract.py +7 -5
- edsl/questions/QuestionFreeText.py +3 -2
- edsl/questions/QuestionList.py +18 -10
- edsl/questions/QuestionMultipleChoice.py +23 -67
- edsl/questions/QuestionNumerical.py +4 -2
- edsl/questions/QuestionRank.py +17 -7
- edsl/questions/{response_validator_abc.py → ResponseValidatorABC.py} +26 -40
- edsl/questions/SimpleAskMixin.py +3 -4
- edsl/questions/__init__.py +1 -2
- edsl/questions/derived/QuestionLinearScale.py +3 -6
- edsl/questions/derived/QuestionTopK.py +1 -1
- edsl/questions/descriptors.py +3 -17
- edsl/questions/question_registry.py +1 -1
- edsl/results/CSSParameterizer.py +1 -1
- edsl/results/Dataset.py +7 -170
- edsl/results/DatasetExportMixin.py +305 -168
- edsl/results/DatasetTree.py +8 -28
- edsl/results/Result.py +206 -298
- edsl/results/Results.py +131 -149
- edsl/results/ResultsDBMixin.py +238 -0
- edsl/results/ResultsExportMixin.py +0 -2
- edsl/results/{results_selector.py → Selector.py} +13 -23
- edsl/results/TableDisplay.py +171 -98
- edsl/results/__init__.py +1 -1
- edsl/scenarios/FileStore.py +239 -150
- edsl/scenarios/Scenario.py +193 -90
- edsl/scenarios/ScenarioHtmlMixin.py +3 -4
- edsl/scenarios/{scenario_join.py → ScenarioJoin.py} +6 -10
- edsl/scenarios/ScenarioList.py +244 -415
- edsl/scenarios/ScenarioListExportMixin.py +7 -0
- edsl/scenarios/ScenarioListPdfMixin.py +37 -15
- edsl/scenarios/__init__.py +2 -1
- edsl/study/ObjectEntry.py +1 -1
- edsl/study/SnapShot.py +1 -1
- edsl/study/Study.py +12 -5
- edsl/surveys/Rule.py +4 -5
- edsl/surveys/RuleCollection.py +27 -25
- edsl/surveys/Survey.py +791 -270
- edsl/surveys/SurveyCSS.py +8 -20
- edsl/surveys/{SurveyFlowVisualization.py → SurveyFlowVisualizationMixin.py} +9 -11
- edsl/surveys/__init__.py +2 -4
- edsl/surveys/descriptors.py +2 -6
- edsl/surveys/instructions/ChangeInstruction.py +2 -1
- edsl/surveys/instructions/Instruction.py +13 -4
- edsl/surveys/instructions/InstructionCollection.py +6 -11
- 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/utilities.py +23 -35
- {edsl-0.1.39.dist-info → edsl-0.1.39.dev1.dist-info}/METADATA +10 -12
- edsl-0.1.39.dev1.dist-info/RECORD +277 -0
- {edsl-0.1.39.dist-info → edsl-0.1.39.dev1.dist-info}/WHEEL +1 -1
- edsl/agents/QuestionInstructionPromptBuilder.py +0 -128
- edsl/agents/QuestionTemplateReplacementsBuilder.py +0 -137
- edsl/agents/question_option_processor.py +0 -172
- edsl/coop/CoopFunctionsMixin.py +0 -15
- edsl/coop/ExpectedParrotKeyHandler.py +0 -125
- edsl/exceptions/inference_services.py +0 -5
- edsl/inference_services/AvailableModelCacheHandler.py +0 -184
- edsl/inference_services/AvailableModelFetcher.py +0 -215
- edsl/inference_services/ServiceAvailability.py +0 -135
- edsl/inference_services/data_structures.py +0 -134
- edsl/jobs/AnswerQuestionFunctionConstructor.py +0 -223
- edsl/jobs/FetchInvigilator.py +0 -47
- edsl/jobs/InterviewTaskManager.py +0 -98
- edsl/jobs/InterviewsConstructor.py +0 -50
- edsl/jobs/JobsComponentConstructor.py +0 -189
- edsl/jobs/JobsRemoteInferenceLogger.py +0 -239
- edsl/jobs/RequestTokenEstimator.py +0 -30
- edsl/jobs/async_interview_runner.py +0 -138
- edsl/jobs/buckets/TokenBucketAPI.py +0 -211
- edsl/jobs/buckets/TokenBucketClient.py +0 -191
- edsl/jobs/check_survey_scenario_compatibility.py +0 -85
- edsl/jobs/data_structures.py +0 -120
- edsl/jobs/decorators.py +0 -35
- edsl/jobs/jobs_status_enums.py +0 -9
- edsl/jobs/loggers/HTMLTableJobLogger.py +0 -304
- edsl/jobs/results_exceptions_handler.py +0 -98
- edsl/language_models/ComputeCost.py +0 -63
- edsl/language_models/PriceManager.py +0 -127
- edsl/language_models/RawResponseHandler.py +0 -106
- edsl/language_models/ServiceDataSources.py +0 -0
- edsl/language_models/key_management/KeyLookup.py +0 -63
- edsl/language_models/key_management/KeyLookupBuilder.py +0 -273
- edsl/language_models/key_management/KeyLookupCollection.py +0 -38
- edsl/language_models/key_management/__init__.py +0 -0
- edsl/language_models/key_management/models.py +0 -131
- edsl/language_models/model.py +0 -256
- edsl/notebooks/NotebookToLaTeX.py +0 -142
- edsl/questions/ExceptionExplainer.py +0 -77
- edsl/questions/HTMLQuestion.py +0 -103
- edsl/questions/QuestionMatrix.py +0 -265
- edsl/questions/data_structures.py +0 -20
- edsl/questions/loop_processor.py +0 -149
- edsl/questions/response_validator_factory.py +0 -34
- edsl/questions/templates/matrix/__init__.py +0 -1
- edsl/questions/templates/matrix/answering_instructions.jinja +0 -5
- edsl/questions/templates/matrix/question_presentation.jinja +0 -20
- edsl/results/MarkdownToDocx.py +0 -122
- edsl/results/MarkdownToPDF.py +0 -111
- edsl/results/TextEditor.py +0 -50
- edsl/results/file_exports.py +0 -252
- edsl/results/smart_objects.py +0 -96
- edsl/results/table_data_class.py +0 -12
- edsl/results/table_renderers.py +0 -118
- edsl/scenarios/ConstructDownloadLink.py +0 -109
- edsl/scenarios/DocumentChunker.py +0 -102
- edsl/scenarios/DocxScenario.py +0 -16
- edsl/scenarios/PdfExtractor.py +0 -40
- edsl/scenarios/directory_scanner.py +0 -96
- edsl/scenarios/file_methods.py +0 -85
- edsl/scenarios/handlers/__init__.py +0 -13
- edsl/scenarios/handlers/csv.py +0 -49
- edsl/scenarios/handlers/docx.py +0 -76
- edsl/scenarios/handlers/html.py +0 -37
- edsl/scenarios/handlers/json.py +0 -111
- edsl/scenarios/handlers/latex.py +0 -5
- edsl/scenarios/handlers/md.py +0 -51
- edsl/scenarios/handlers/pdf.py +0 -68
- edsl/scenarios/handlers/png.py +0 -39
- edsl/scenarios/handlers/pptx.py +0 -105
- edsl/scenarios/handlers/py.py +0 -294
- edsl/scenarios/handlers/sql.py +0 -313
- edsl/scenarios/handlers/sqlite.py +0 -149
- edsl/scenarios/handlers/txt.py +0 -33
- edsl/scenarios/scenario_selector.py +0 -156
- edsl/surveys/ConstructDAG.py +0 -92
- edsl/surveys/EditSurvey.py +0 -221
- edsl/surveys/InstructionHandler.py +0 -100
- edsl/surveys/MemoryManagement.py +0 -72
- edsl/surveys/RuleManager.py +0 -172
- edsl/surveys/Simulator.py +0 -75
- edsl/surveys/SurveyToApp.py +0 -141
- edsl/utilities/PrettyList.py +0 -56
- edsl/utilities/is_notebook.py +0 -18
- edsl/utilities/is_valid_variable_name.py +0 -11
- edsl/utilities/remove_edsl_version.py +0 -24
- edsl-0.1.39.dist-info/RECORD +0 -358
- /edsl/questions/{register_questions_meta.py → RegisterQuestionsMeta.py} +0 -0
- /edsl/results/{results_fetch_mixin.py → ResultsFetchMixin.py} +0 -0
- /edsl/results/{results_tools_mixin.py → ResultsToolsMixin.py} +0 -0
- {edsl-0.1.39.dist-info → edsl-0.1.39.dev1.dist-info}/LICENSE +0 -0
edsl/questions/QuestionBase.py
CHANGED
@@ -2,35 +2,31 @@
|
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
from abc import ABC, abstractmethod
|
5
|
-
from typing import Any, Type, Optional, List, Callable, Union, TypedDict
|
5
|
+
from typing import Any, Type, Optional, List, Callable, Union, TypedDict
|
6
|
+
import copy
|
6
7
|
|
7
|
-
from edsl.exceptions
|
8
|
+
from edsl.exceptions import (
|
9
|
+
QuestionResponseValidationError,
|
10
|
+
QuestionAnswerValidationError,
|
8
11
|
QuestionSerializationError,
|
9
12
|
)
|
10
13
|
from edsl.questions.descriptors import QuestionNameDescriptor, QuestionTextDescriptor
|
11
14
|
|
12
|
-
|
13
|
-
from edsl.questions.
|
14
|
-
from edsl.
|
15
|
+
|
16
|
+
from edsl.questions.AnswerValidatorMixin import AnswerValidatorMixin
|
17
|
+
from edsl.questions.RegisterQuestionsMeta import RegisterQuestionsMeta
|
18
|
+
from edsl.Base import PersistenceMixin, RichPrintingMixin
|
15
19
|
from edsl.BaseDiff import BaseDiff, BaseDiffCollection
|
16
20
|
|
17
21
|
from edsl.questions.SimpleAskMixin import SimpleAskMixin
|
18
22
|
from edsl.questions.QuestionBasePromptsMixin import QuestionBasePromptsMixin
|
19
|
-
from edsl.questions.
|
20
|
-
from edsl.utilities.
|
21
|
-
|
22
|
-
if TYPE_CHECKING:
|
23
|
-
from edsl.questions.response_validator_abc import ResponseValidatorABC
|
24
|
-
from edsl.language_models.LanguageModel import LanguageModel
|
25
|
-
from edsl.results.Results import Results
|
26
|
-
from edsl.agents.Agent import Agent
|
27
|
-
from edsl.surveys.Survey import Survey
|
28
|
-
from edsl.jobs.Jobs import Jobs
|
23
|
+
from edsl.questions.QuestionBaseGenMixin import QuestionBaseGenMixin
|
24
|
+
from edsl.utilities.decorators import add_edsl_version, remove_edsl_version
|
29
25
|
|
30
26
|
|
31
27
|
class QuestionBase(
|
32
28
|
PersistenceMixin,
|
33
|
-
|
29
|
+
RichPrintingMixin,
|
34
30
|
SimpleAskMixin,
|
35
31
|
QuestionBasePromptsMixin,
|
36
32
|
QuestionBaseGenMixin,
|
@@ -40,14 +36,6 @@ class QuestionBase(
|
|
40
36
|
):
|
41
37
|
"""ABC for the Question class. All questions inherit from this class.
|
42
38
|
Some of the constraints on child questions are defined in the RegisterQuestionsMeta metaclass.
|
43
|
-
|
44
|
-
|
45
|
-
Every child class wiill have class attributes of question_type, _response_model and response_validator_class e.g.,
|
46
|
-
|
47
|
-
question_type = "free_text"
|
48
|
-
_response_model = FreeTextResponse
|
49
|
-
response_validator_class = FreeTextResponseValidator
|
50
|
-
|
51
39
|
"""
|
52
40
|
|
53
41
|
question_name: str = QuestionNameDescriptor()
|
@@ -57,16 +45,36 @@ class QuestionBase(
|
|
57
45
|
_question_presentation = None
|
58
46
|
|
59
47
|
@property
|
60
|
-
def
|
48
|
+
def response_model(self) -> type["BaseModel"]:
|
49
|
+
if self._response_model is not None:
|
50
|
+
return self._response_model
|
51
|
+
else:
|
52
|
+
return self.create_response_model()
|
53
|
+
|
54
|
+
# region: Validation and simulation methods
|
55
|
+
@property
|
56
|
+
def response_validator(self) -> "ResponseValidatorBase":
|
61
57
|
"""Return the response validator."""
|
62
|
-
|
58
|
+
params = (
|
59
|
+
{
|
60
|
+
"response_model": self.response_model,
|
61
|
+
}
|
62
|
+
| {k: getattr(self, k) for k in self.validator_parameters}
|
63
|
+
| {"exception_to_throw": getattr(self, "exception_to_throw", None)}
|
64
|
+
| {"override_answer": getattr(self, "override_answer", None)}
|
65
|
+
)
|
66
|
+
return self.response_validator_class(**params)
|
63
67
|
|
64
|
-
|
65
|
-
|
68
|
+
@property
|
69
|
+
def validator_parameters(self) -> list[str]:
|
70
|
+
"""Return the parameters required for the response validator.
|
66
71
|
|
67
|
-
|
68
|
-
|
69
|
-
|
72
|
+
>>> from edsl import QuestionMultipleChoice as Q
|
73
|
+
>>> Q.example().validator_parameters
|
74
|
+
['question_options', 'use_code']
|
75
|
+
|
76
|
+
"""
|
77
|
+
return self.response_validator_class.required_params
|
70
78
|
|
71
79
|
@property
|
72
80
|
def fake_data_factory(self):
|
@@ -74,7 +82,8 @@ class QuestionBase(
|
|
74
82
|
if not hasattr(self, "_fake_data_factory"):
|
75
83
|
from polyfactory.factories.pydantic_factory import ModelFactory
|
76
84
|
|
77
|
-
class FakeData(ModelFactory[self.response_model]):
|
85
|
+
class FakeData(ModelFactory[self.response_model]):
|
86
|
+
...
|
78
87
|
|
79
88
|
self._fake_data_factory = FakeData
|
80
89
|
return self._fake_data_factory
|
@@ -101,14 +110,17 @@ class QuestionBase(
|
|
101
110
|
self, answer: dict, replacement_dict: dict = None
|
102
111
|
) -> ValidatedAnswer:
|
103
112
|
"""Validate the answer.
|
104
|
-
>>> from edsl.exceptions
|
105
|
-
>>> from edsl
|
113
|
+
>>> from edsl.exceptions import QuestionAnswerValidationError
|
114
|
+
>>> from edsl import QuestionFreeText as Q
|
106
115
|
>>> Q.example()._validate_answer({'answer': 'Hello', 'generated_tokens': 'Hello'})
|
107
116
|
{'answer': 'Hello', 'generated_tokens': 'Hello'}
|
108
117
|
"""
|
109
118
|
|
110
119
|
return self.response_validator.validate(answer, replacement_dict)
|
111
120
|
|
121
|
+
# endregion
|
122
|
+
|
123
|
+
# region: Serialization methods
|
112
124
|
@property
|
113
125
|
def name(self) -> str:
|
114
126
|
"Helper function so questions and instructions can use the same access method"
|
@@ -129,7 +141,7 @@ class QuestionBase(
|
|
129
141
|
def data(self) -> dict:
|
130
142
|
"""Return a dictionary of question attributes **except** for question_type.
|
131
143
|
|
132
|
-
>>> from edsl
|
144
|
+
>>> from edsl import QuestionFreeText as Q
|
133
145
|
>>> Q.example().data
|
134
146
|
{'question_name': 'how_are_you', 'question_text': 'How are you?'}
|
135
147
|
"""
|
@@ -171,10 +183,10 @@ class QuestionBase(
|
|
171
183
|
|
172
184
|
return candidate_data
|
173
185
|
|
174
|
-
def to_dict(self, add_edsl_version
|
186
|
+
def to_dict(self, add_edsl_version=True):
|
175
187
|
"""Convert the question to a dictionary that includes the question type (used in deserialization).
|
176
188
|
|
177
|
-
>>> from edsl
|
189
|
+
>>> from edsl import QuestionFreeText as Q; Q.example().to_dict(add_edsl_version = False)
|
178
190
|
{'question_name': 'how_are_you', 'question_text': 'How are you?', 'question_type': 'free_text'}
|
179
191
|
"""
|
180
192
|
candidate_data = self.data.copy()
|
@@ -225,6 +237,9 @@ class QuestionBase(
|
|
225
237
|
|
226
238
|
return question_class(**local_data)
|
227
239
|
|
240
|
+
# endregion
|
241
|
+
|
242
|
+
# region: Running methods
|
228
243
|
@classmethod
|
229
244
|
def _get_test_model(self, canned_response: Optional[str] = None) -> "LanguageModel":
|
230
245
|
"""Get a test model for the question."""
|
@@ -249,10 +264,12 @@ class QuestionBase(
|
|
249
264
|
>>> m.execute_model_call("", "")
|
250
265
|
{'message': [{'text': "Yo, what's up?"}], 'usage': {'prompt_tokens': 1, 'completion_tokens': 1}}
|
251
266
|
>>> Q.run_example(show_answer = True, model = m, disable_remote_cache = True, disable_remote_inference = True)
|
252
|
-
|
267
|
+
answer.how_are_you
|
268
|
+
--------------------
|
269
|
+
Yo, what's up?
|
253
270
|
"""
|
254
271
|
if model is None:
|
255
|
-
from edsl
|
272
|
+
from edsl import Model
|
256
273
|
|
257
274
|
model = Model()
|
258
275
|
results = (
|
@@ -265,28 +282,26 @@ class QuestionBase(
|
|
265
282
|
)
|
266
283
|
)
|
267
284
|
if show_answer:
|
268
|
-
return results.select("answer.*")
|
285
|
+
return results.select("answer.*").print()
|
269
286
|
else:
|
270
287
|
return results
|
271
288
|
|
272
289
|
def __call__(
|
273
290
|
self,
|
274
|
-
just_answer
|
275
|
-
model
|
276
|
-
agent
|
291
|
+
just_answer=True,
|
292
|
+
model=None,
|
293
|
+
agent=None,
|
277
294
|
disable_remote_cache: bool = False,
|
278
295
|
disable_remote_inference: bool = False,
|
279
|
-
verbose: bool = False,
|
280
296
|
**kwargs,
|
281
|
-
)
|
297
|
+
):
|
282
298
|
"""Call the question.
|
283
299
|
|
284
300
|
|
285
301
|
>>> from edsl import QuestionFreeText as Q
|
286
|
-
>>>
|
287
|
-
>>> m = Model("test", canned_response = "Yo, what's up?")
|
302
|
+
>>> m = Q._get_test_model(canned_response = "Yo, what's up?")
|
288
303
|
>>> q = Q(question_name = "color", question_text = "What is your favorite color?")
|
289
|
-
>>> q(model = m, disable_remote_cache = True, disable_remote_inference = True
|
304
|
+
>>> q(model = m, disable_remote_cache = True, disable_remote_inference = True)
|
290
305
|
"Yo, what's up?"
|
291
306
|
|
292
307
|
"""
|
@@ -295,7 +310,7 @@ class QuestionBase(
|
|
295
310
|
model=model,
|
296
311
|
agent=agent,
|
297
312
|
**kwargs,
|
298
|
-
|
313
|
+
cache=False,
|
299
314
|
disable_remote_cache=disable_remote_cache,
|
300
315
|
disable_remote_inference=disable_remote_inference,
|
301
316
|
)
|
@@ -306,16 +321,15 @@ class QuestionBase(
|
|
306
321
|
|
307
322
|
def run(self, *args, **kwargs) -> "Results":
|
308
323
|
"""Turn a single question into a survey and runs it."""
|
309
|
-
|
324
|
+
from edsl.surveys.Survey import Survey
|
310
325
|
|
311
|
-
|
312
|
-
|
313
|
-
return self.to_survey().to_jobs().using(*args, **kwargs)
|
326
|
+
s = self.to_survey()
|
327
|
+
return s.run(*args, **kwargs)
|
314
328
|
|
315
329
|
async def run_async(
|
316
330
|
self,
|
317
331
|
just_answer: bool = True,
|
318
|
-
model: Optional["
|
332
|
+
model: Optional["Model"] = None,
|
319
333
|
agent: Optional["Agent"] = None,
|
320
334
|
disable_remote_inference: bool = False,
|
321
335
|
**kwargs,
|
@@ -323,10 +337,10 @@ class QuestionBase(
|
|
323
337
|
"""Call the question asynchronously.
|
324
338
|
|
325
339
|
>>> import asyncio
|
326
|
-
>>> from edsl
|
340
|
+
>>> from edsl import QuestionFreeText as Q
|
327
341
|
>>> m = Q._get_test_model(canned_response = "Blue")
|
328
342
|
>>> q = Q(question_name = "color", question_text = "What is your favorite color?")
|
329
|
-
>>> async def test_run_async(): result = await q.run_async(model=m, disable_remote_inference = True
|
343
|
+
>>> async def test_run_async(): result = await q.run_async(model=m, disable_remote_inference = True); print(result)
|
330
344
|
>>> asyncio.run(test_run_async())
|
331
345
|
Blue
|
332
346
|
"""
|
@@ -342,6 +356,27 @@ class QuestionBase(
|
|
342
356
|
else:
|
343
357
|
return results
|
344
358
|
|
359
|
+
# endregion
|
360
|
+
|
361
|
+
# region: Magic methods
|
362
|
+
def _repr_html_(self):
|
363
|
+
# from edsl.utilities.utilities import data_to_html
|
364
|
+
|
365
|
+
data = self.to_dict(add_edsl_version=False)
|
366
|
+
# keys = list(data.keys())
|
367
|
+
# values = list(data.values())
|
368
|
+
from tabulate import tabulate
|
369
|
+
|
370
|
+
return tabulate(data.items(), headers=["keys", "values"], tablefmt="html")
|
371
|
+
|
372
|
+
# try:
|
373
|
+
# _ = data.pop("edsl_version")
|
374
|
+
# _ = data.pop("edsl_class_name")
|
375
|
+
# except KeyError:
|
376
|
+
# print("Serialized question lacks edsl version, but is should have it.")
|
377
|
+
|
378
|
+
# return data_to_html(data)
|
379
|
+
|
345
380
|
def __getitem__(self, key: str) -> Any:
|
346
381
|
"""Get an attribute of the question so it can be treated like a dictionary.
|
347
382
|
|
@@ -349,10 +384,7 @@ class QuestionBase(
|
|
349
384
|
>>> Q.example()['question_text']
|
350
385
|
'How are you?'
|
351
386
|
"""
|
352
|
-
|
353
|
-
return getattr(self, key)
|
354
|
-
except TypeError:
|
355
|
-
raise KeyError(f"Question has no attribute {key} of type {type(key)}")
|
387
|
+
return getattr(self, key)
|
356
388
|
|
357
389
|
def __repr__(self) -> str:
|
358
390
|
"""Return a string representation of the question. Should be able to be used to reconstruct the question.
|
@@ -382,7 +414,9 @@ class QuestionBase(
|
|
382
414
|
False
|
383
415
|
|
384
416
|
"""
|
385
|
-
|
417
|
+
if not isinstance(other, QuestionBase):
|
418
|
+
return False
|
419
|
+
return self.to_dict() == other.to_dict()
|
386
420
|
|
387
421
|
def __sub__(self, other) -> BaseDiff:
|
388
422
|
"""Return the difference between two objects.
|
@@ -399,18 +433,35 @@ class QuestionBase(
|
|
399
433
|
def __add__(self, other_question_or_diff):
|
400
434
|
"""
|
401
435
|
Compose two questions into a single question.
|
436
|
+
|
437
|
+
TODO: Probably getting deprecated.
|
438
|
+
|
402
439
|
"""
|
403
440
|
if isinstance(other_question_or_diff, BaseDiff) or isinstance(
|
404
441
|
other_question_or_diff, BaseDiffCollection
|
405
442
|
):
|
406
443
|
return other_question_or_diff.apply(self)
|
407
444
|
|
445
|
+
# from edsl.questions import compose_questions
|
446
|
+
# return compose_questions(self, other_question_or_diff)
|
447
|
+
|
448
|
+
# def _validate_response(self, response):
|
449
|
+
# """Validate the response from the LLM. Behavior depends on the question type."""
|
450
|
+
# if "answer" not in response:
|
451
|
+
# raise QuestionResponseValidationError(
|
452
|
+
# "Response from LLM does not have an answer"
|
453
|
+
# )
|
454
|
+
# return response
|
455
|
+
|
408
456
|
def _translate_answer_code_to_answer(
|
409
457
|
self, answer, scenario: Optional["Scenario"] = None
|
410
458
|
):
|
411
459
|
"""There is over-ridden by child classes that ask for codes."""
|
412
460
|
return answer
|
413
461
|
|
462
|
+
# endregion
|
463
|
+
|
464
|
+
# region: Forward methods
|
414
465
|
def add_question(self, other: QuestionBase) -> "Survey":
|
415
466
|
"""Add a question to this question by turning them into a survey with two questions.
|
416
467
|
|
@@ -420,7 +471,10 @@ class QuestionBase(
|
|
420
471
|
>>> len(s.questions)
|
421
472
|
2
|
422
473
|
"""
|
423
|
-
|
474
|
+
from edsl.surveys.Survey import Survey
|
475
|
+
|
476
|
+
s = Survey([self, other])
|
477
|
+
return s
|
424
478
|
|
425
479
|
def to_survey(self) -> "Survey":
|
426
480
|
"""Turn a single question into a survey.
|
@@ -430,7 +484,8 @@ class QuestionBase(
|
|
430
484
|
"""
|
431
485
|
from edsl.surveys.Survey import Survey
|
432
486
|
|
433
|
-
|
487
|
+
s = Survey([self])
|
488
|
+
return s
|
434
489
|
|
435
490
|
def by(self, *args) -> "Jobs":
|
436
491
|
"""Turn a single question into a survey and then a Job."""
|
@@ -439,6 +494,15 @@ class QuestionBase(
|
|
439
494
|
s = Survey([self])
|
440
495
|
return s.by(*args)
|
441
496
|
|
497
|
+
# endregion
|
498
|
+
|
499
|
+
# region: Display methods
|
500
|
+
def print(self):
|
501
|
+
from rich import print_json
|
502
|
+
import json
|
503
|
+
|
504
|
+
print_json(json.dumps(self.to_dict()))
|
505
|
+
|
442
506
|
def human_readable(self) -> str:
|
443
507
|
"""Print the question in a human readable format.
|
444
508
|
|
@@ -465,15 +529,97 @@ class QuestionBase(
|
|
465
529
|
width: Optional[int] = None,
|
466
530
|
iframe=False,
|
467
531
|
):
|
468
|
-
|
532
|
+
"""Return the question in HTML format."""
|
533
|
+
from jinja2 import Template
|
534
|
+
|
535
|
+
if scenario is None:
|
536
|
+
scenario = {}
|
537
|
+
|
538
|
+
prior_answers_dict = {}
|
539
|
+
|
540
|
+
if isinstance(answers, dict):
|
541
|
+
for key, value in answers.items():
|
542
|
+
if not key.endswith("_comment") and not key.endswith(
|
543
|
+
"_generated_tokens"
|
544
|
+
):
|
545
|
+
prior_answers_dict[key] = {"answer": value}
|
546
|
+
|
547
|
+
# breakpoint()
|
548
|
+
|
549
|
+
base_template = """
|
550
|
+
<div id="{{ question_name }}" class="survey_question" data-type="{{ question_type }}">
|
551
|
+
{% if include_question_name %}
|
552
|
+
<p>question_name: {{ question_name }}</p>
|
553
|
+
{% endif %}
|
554
|
+
<p class="question_text">{{ question_text }}</p>
|
555
|
+
{{ question_content }}
|
556
|
+
</div>
|
557
|
+
"""
|
558
|
+
if not hasattr(self, "question_type"):
|
559
|
+
self.question_type = "unknown"
|
469
560
|
|
470
|
-
|
471
|
-
|
472
|
-
|
561
|
+
if hasattr(self, "question_html_content"):
|
562
|
+
question_content = self.question_html_content
|
563
|
+
else:
|
564
|
+
question_content = Template("")
|
565
|
+
|
566
|
+
base_template = Template(base_template)
|
567
|
+
|
568
|
+
context = {
|
569
|
+
"scenario": scenario,
|
570
|
+
"agent": agent,
|
571
|
+
} | prior_answers_dict
|
572
|
+
|
573
|
+
# Render the question text
|
574
|
+
try:
|
575
|
+
question_text = Template(self.question_text).render(context)
|
576
|
+
except Exception as e:
|
577
|
+
print(
|
578
|
+
f"Error rendering question: question_text = {self.question_text}, error = {e}"
|
579
|
+
)
|
580
|
+
question_text = self.question_text
|
581
|
+
|
582
|
+
try:
|
583
|
+
question_content = Template(question_content).render(context)
|
584
|
+
except Exception as e:
|
585
|
+
print(
|
586
|
+
f"Error rendering question: question_content = {question_content}, error = {e}"
|
587
|
+
)
|
588
|
+
question_content = question_content
|
589
|
+
|
590
|
+
try:
|
591
|
+
params = {
|
592
|
+
"question_name": self.question_name,
|
593
|
+
"question_text": question_text,
|
594
|
+
"question_type": self.question_type,
|
595
|
+
"question_content": question_content,
|
596
|
+
"include_question_name": include_question_name,
|
597
|
+
}
|
598
|
+
except Exception as e:
|
599
|
+
raise ValueError(
|
600
|
+
f"Error rendering question: params = {params}, error = {e}"
|
601
|
+
)
|
602
|
+
rendered_html = base_template.render(**params)
|
603
|
+
|
604
|
+
if iframe:
|
605
|
+
import html
|
606
|
+
from IPython.display import display, HTML
|
607
|
+
|
608
|
+
height = height or 200
|
609
|
+
width = width or 600
|
610
|
+
escaped_output = html.escape(rendered_html)
|
611
|
+
# escaped_output = rendered_html
|
612
|
+
iframe = f""""
|
613
|
+
<iframe srcdoc="{ escaped_output }" style="width: {width}px; height: {height}px;"></iframe>
|
614
|
+
"""
|
615
|
+
display(HTML(iframe))
|
616
|
+
return None
|
617
|
+
|
618
|
+
return rendered_html
|
473
619
|
|
474
620
|
@classmethod
|
475
621
|
def example_model(cls):
|
476
|
-
from edsl
|
622
|
+
from edsl import Model
|
477
623
|
|
478
624
|
q = cls.example()
|
479
625
|
m = Model("test", canned_response=cls._simulate_answer(q)["answer"])
|
@@ -1,16 +1,11 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
import copy
|
3
3
|
import itertools
|
4
|
-
from typing import Optional, List, Callable, Type
|
5
|
-
|
6
|
-
if TYPE_CHECKING:
|
7
|
-
from edsl.questions.QuestionBase import QuestionBase
|
8
|
-
from edsl.scenarios.ScenarioList import ScenarioList
|
4
|
+
from typing import Optional, List, Callable, Type
|
5
|
+
from typing import TypeVar
|
9
6
|
|
10
7
|
|
11
8
|
class QuestionBaseGenMixin:
|
12
|
-
"""Mixin for QuestionBase."""
|
13
|
-
|
14
9
|
def copy(self) -> QuestionBase:
|
15
10
|
"""Return a deep copy of the question.
|
16
11
|
|
@@ -26,7 +21,7 @@ class QuestionBaseGenMixin:
|
|
26
21
|
def option_permutations(self) -> list[QuestionBase]:
|
27
22
|
"""Return a list of questions with all possible permutations of the options.
|
28
23
|
|
29
|
-
>>> from edsl
|
24
|
+
>>> from edsl import QuestionMultipleChoice as Q
|
30
25
|
>>> len(Q.example().option_permutations())
|
31
26
|
24
|
32
27
|
"""
|
@@ -44,60 +39,66 @@ class QuestionBaseGenMixin:
|
|
44
39
|
questions.append(question)
|
45
40
|
return questions
|
46
41
|
|
47
|
-
def draw(self) -> "QuestionBase":
|
48
|
-
"""Return a new question with a randomly selected permutation of the options.
|
49
|
-
|
50
|
-
If the question has no options, returns a copy of the original question.
|
51
|
-
|
52
|
-
>>> from edsl.questions.QuestionMultipleChoice import QuestionMultipleChoice as Q
|
53
|
-
>>> q = Q.example()
|
54
|
-
>>> drawn = q.draw()
|
55
|
-
>>> len(drawn.question_options) == len(q.question_options)
|
56
|
-
True
|
57
|
-
>>> q is drawn
|
58
|
-
False
|
59
|
-
"""
|
60
|
-
|
61
|
-
if not hasattr(self, "question_options"):
|
62
|
-
return copy.deepcopy(self)
|
63
|
-
|
64
|
-
import random
|
65
|
-
|
66
|
-
question = copy.deepcopy(self)
|
67
|
-
question.question_options = list(
|
68
|
-
random.sample(self.question_options, len(self.question_options))
|
69
|
-
)
|
70
|
-
return question
|
71
|
-
|
72
42
|
def loop(self, scenario_list: ScenarioList) -> List[QuestionBase]:
|
73
43
|
"""Return a list of questions with the question name modified for each scenario.
|
74
44
|
|
75
45
|
:param scenario_list: The list of scenarios to loop through.
|
76
46
|
|
77
|
-
>>> from edsl
|
78
|
-
>>> from edsl
|
47
|
+
>>> from edsl import QuestionFreeText
|
48
|
+
>>> from edsl import ScenarioList
|
79
49
|
>>> q = QuestionFreeText(question_text = "What are your thoughts on: {{ subject}}?", question_name = "base_{{subject}}")
|
80
50
|
>>> len(q.loop(ScenarioList.from_list("subject", ["Math", "Economics", "Chemistry"])))
|
81
51
|
3
|
82
|
-
"""
|
83
|
-
from edsl.questions.loop_processor import LoopProcessor
|
84
52
|
|
85
|
-
|
86
|
-
|
53
|
+
"""
|
54
|
+
from jinja2 import Environment
|
55
|
+
from edsl.questions.QuestionBase import QuestionBase
|
87
56
|
|
88
|
-
|
89
|
-
|
57
|
+
starting_name = self.question_name
|
58
|
+
questions = []
|
59
|
+
for index, scenario in enumerate(scenario_list):
|
60
|
+
env = Environment()
|
61
|
+
new_data = self.to_dict().copy()
|
62
|
+
for key, value in [(k, v) for k, v in new_data.items() if v is not None]:
|
63
|
+
if (
|
64
|
+
isinstance(value, str) or isinstance(value, int)
|
65
|
+
) and key != "question_options":
|
66
|
+
new_data[key] = env.from_string(value).render(scenario)
|
67
|
+
elif isinstance(value, list):
|
68
|
+
new_data[key] = [
|
69
|
+
env.from_string(v).render(scenario) if isinstance(v, str) else v
|
70
|
+
for v in value
|
71
|
+
]
|
72
|
+
elif isinstance(value, dict):
|
73
|
+
new_data[key] = {
|
74
|
+
(
|
75
|
+
env.from_string(k).render(scenario)
|
76
|
+
if isinstance(k, str)
|
77
|
+
else k
|
78
|
+
): (
|
79
|
+
env.from_string(v).render(scenario)
|
80
|
+
if isinstance(v, str)
|
81
|
+
else v
|
82
|
+
)
|
83
|
+
for k, v in value.items()
|
84
|
+
}
|
85
|
+
elif key == "question_options" and isinstance(value, str):
|
86
|
+
new_data[key] = value
|
87
|
+
else:
|
88
|
+
raise ValueError(
|
89
|
+
f"Unexpected value type: {type(value)} for key '{key}'"
|
90
|
+
)
|
90
91
|
|
91
|
-
|
92
|
+
if new_data["question_name"] == starting_name:
|
93
|
+
new_data["question_name"] = new_data["question_name"] + f"_{index}"
|
92
94
|
|
93
|
-
|
94
|
-
|
95
|
-
>>> q.render({"thing": "color"})
|
96
|
-
Question('free_text', question_name = \"""color\""", question_text = \"""What is your favorite color?\""")
|
95
|
+
questions.append(QuestionBase.from_dict(new_data))
|
96
|
+
return questions
|
97
97
|
|
98
|
-
|
98
|
+
def render(self, replacement_dict: dict) -> "QuestionBase":
|
99
|
+
"""Render the question components as jinja2 templates with the replacement dictionary."""
|
99
100
|
from jinja2 import Environment
|
100
|
-
from edsl
|
101
|
+
from edsl import Scenario
|
101
102
|
|
102
103
|
strings_only_replacement_dict = {
|
103
104
|
k: v for k, v in replacement_dict.items() if not isinstance(v, Scenario)
|
@@ -122,23 +123,15 @@ class QuestionBaseGenMixin:
|
|
122
123
|
|
123
124
|
return self.apply_function(render_string)
|
124
125
|
|
125
|
-
def apply_function(
|
126
|
-
self, func: Callable, exclude_components: List[str] = None
|
127
|
-
) -> QuestionBase:
|
126
|
+
def apply_function(self, func: Callable, exclude_components=None) -> QuestionBase:
|
128
127
|
"""Apply a function to the question parts
|
129
128
|
|
130
|
-
:param func: The function to apply to the question parts.
|
131
|
-
:param exclude_components: The components to exclude from the function application.
|
132
|
-
|
133
129
|
>>> from edsl.questions import QuestionFreeText
|
134
130
|
>>> q = QuestionFreeText(question_name = "color", question_text = "What is your favorite color?")
|
135
131
|
>>> shouting = lambda x: x.upper()
|
136
132
|
>>> q.apply_function(shouting)
|
137
133
|
Question('free_text', question_name = \"""color\""", question_text = \"""WHAT IS YOUR FAVORITE COLOR?\""")
|
138
134
|
|
139
|
-
>>> q.apply_function(shouting, exclude_components = ["question_type"])
|
140
|
-
Question('free_text', question_name = \"""COLOR\""", question_text = \"""WHAT IS YOUR FAVORITE COLOR?\""")
|
141
|
-
|
142
135
|
"""
|
143
136
|
from edsl.questions.QuestionBase import QuestionBase
|
144
137
|
|
@@ -1,6 +1,8 @@
|
|
1
1
|
from importlib import resources
|
2
2
|
from typing import Optional
|
3
|
+
from edsl.prompts import Prompt
|
3
4
|
from edsl.exceptions.questions import QuestionAnswerValidationError
|
5
|
+
|
4
6
|
from functools import lru_cache
|
5
7
|
|
6
8
|
|
@@ -69,7 +71,7 @@ class QuestionBasePromptsMixin:
|
|
69
71
|
>>> q.get_instructions(model = "gpt3")
|
70
72
|
Prompt(text=\"""{{question_text}}. Answer in valid JSON like so {'answer': 'comment: <>}\""")
|
71
73
|
"""
|
72
|
-
from edsl
|
74
|
+
from edsl import Model
|
73
75
|
|
74
76
|
if not hasattr(self, "_model_instructions"):
|
75
77
|
self._model_instructions = {}
|
@@ -120,8 +122,6 @@ class QuestionBasePromptsMixin:
|
|
120
122
|
template_text = template_manager.get_template(
|
121
123
|
cls.question_type, "answering_instructions.jinja"
|
122
124
|
)
|
123
|
-
from edsl.prompts import Prompt
|
124
|
-
|
125
125
|
return Prompt(text=template_text)
|
126
126
|
|
127
127
|
@classmethod
|
@@ -129,8 +129,6 @@ class QuestionBasePromptsMixin:
|
|
129
129
|
template_text = template_manager.get_template(
|
130
130
|
cls.question_type, "question_presentation.jinja"
|
131
131
|
)
|
132
|
-
from edsl.prompts import Prompt
|
133
|
-
|
134
132
|
return Prompt(text=template_text)
|
135
133
|
|
136
134
|
@property
|
@@ -184,8 +182,6 @@ class QuestionBasePromptsMixin:
|
|
184
182
|
@property
|
185
183
|
def new_default_instructions(self) -> "Prompt":
|
186
184
|
"This is set up as a property because there are mutable question values that determine how it is rendered."
|
187
|
-
from edsl.prompts import Prompt
|
188
|
-
|
189
185
|
return Prompt(self.question_presentation) + Prompt(self.answering_instructions)
|
190
186
|
|
191
187
|
@property
|