edsl 0.1.38.dev4__py3-none-any.whl → 0.1.39__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- edsl/Base.py +197 -116
- edsl/__init__.py +15 -7
- edsl/__version__.py +1 -1
- edsl/agents/Agent.py +351 -147
- edsl/agents/AgentList.py +211 -73
- edsl/agents/Invigilator.py +101 -50
- edsl/agents/InvigilatorBase.py +62 -70
- edsl/agents/PromptConstructor.py +143 -225
- edsl/agents/QuestionInstructionPromptBuilder.py +128 -0
- edsl/agents/QuestionTemplateReplacementsBuilder.py +137 -0
- edsl/agents/__init__.py +0 -1
- edsl/agents/prompt_helpers.py +3 -3
- edsl/agents/question_option_processor.py +172 -0
- edsl/auto/AutoStudy.py +18 -5
- edsl/auto/StageBase.py +53 -40
- edsl/auto/StageQuestions.py +2 -1
- edsl/auto/utilities.py +0 -6
- edsl/config.py +22 -2
- edsl/conversation/car_buying.py +2 -1
- edsl/coop/CoopFunctionsMixin.py +15 -0
- edsl/coop/ExpectedParrotKeyHandler.py +125 -0
- edsl/coop/PriceFetcher.py +1 -1
- edsl/coop/coop.py +125 -47
- edsl/coop/utils.py +14 -14
- edsl/data/Cache.py +45 -27
- edsl/data/CacheEntry.py +12 -15
- edsl/data/CacheHandler.py +31 -12
- edsl/data/RemoteCacheSync.py +154 -46
- edsl/data/__init__.py +4 -3
- edsl/data_transfer_models.py +2 -1
- edsl/enums.py +27 -0
- edsl/exceptions/__init__.py +50 -50
- edsl/exceptions/agents.py +12 -0
- edsl/exceptions/inference_services.py +5 -0
- edsl/exceptions/questions.py +24 -6
- edsl/exceptions/scenarios.py +7 -0
- edsl/inference_services/AnthropicService.py +38 -19
- edsl/inference_services/AvailableModelCacheHandler.py +184 -0
- edsl/inference_services/AvailableModelFetcher.py +215 -0
- edsl/inference_services/AwsBedrock.py +0 -2
- edsl/inference_services/AzureAI.py +0 -2
- edsl/inference_services/GoogleService.py +7 -12
- edsl/inference_services/InferenceServiceABC.py +18 -85
- edsl/inference_services/InferenceServicesCollection.py +120 -79
- edsl/inference_services/MistralAIService.py +0 -3
- edsl/inference_services/OpenAIService.py +47 -35
- edsl/inference_services/PerplexityService.py +0 -3
- edsl/inference_services/ServiceAvailability.py +135 -0
- edsl/inference_services/TestService.py +11 -10
- edsl/inference_services/TogetherAIService.py +5 -3
- edsl/inference_services/data_structures.py +134 -0
- edsl/jobs/AnswerQuestionFunctionConstructor.py +223 -0
- edsl/jobs/Answers.py +1 -14
- edsl/jobs/FetchInvigilator.py +47 -0
- edsl/jobs/InterviewTaskManager.py +98 -0
- edsl/jobs/InterviewsConstructor.py +50 -0
- edsl/jobs/Jobs.py +356 -431
- edsl/jobs/JobsChecks.py +35 -10
- edsl/jobs/JobsComponentConstructor.py +189 -0
- edsl/jobs/JobsPrompts.py +6 -4
- edsl/jobs/JobsRemoteInferenceHandler.py +205 -133
- edsl/jobs/JobsRemoteInferenceLogger.py +239 -0
- edsl/jobs/RequestTokenEstimator.py +30 -0
- edsl/jobs/async_interview_runner.py +138 -0
- edsl/jobs/buckets/BucketCollection.py +44 -3
- edsl/jobs/buckets/TokenBucket.py +53 -21
- edsl/jobs/buckets/TokenBucketAPI.py +211 -0
- edsl/jobs/buckets/TokenBucketClient.py +191 -0
- edsl/jobs/check_survey_scenario_compatibility.py +85 -0
- edsl/jobs/data_structures.py +120 -0
- edsl/jobs/decorators.py +35 -0
- edsl/jobs/interviews/Interview.py +143 -408
- edsl/jobs/jobs_status_enums.py +9 -0
- edsl/jobs/loggers/HTMLTableJobLogger.py +304 -0
- edsl/jobs/results_exceptions_handler.py +98 -0
- edsl/jobs/runners/JobsRunnerAsyncio.py +88 -403
- edsl/jobs/runners/JobsRunnerStatus.py +133 -165
- edsl/jobs/tasks/QuestionTaskCreator.py +21 -19
- edsl/jobs/tasks/TaskHistory.py +38 -18
- edsl/jobs/tasks/task_status_enum.py +0 -2
- edsl/language_models/ComputeCost.py +63 -0
- edsl/language_models/LanguageModel.py +194 -236
- edsl/language_models/ModelList.py +28 -19
- edsl/language_models/PriceManager.py +127 -0
- edsl/language_models/RawResponseHandler.py +106 -0
- edsl/language_models/ServiceDataSources.py +0 -0
- edsl/language_models/__init__.py +1 -2
- edsl/language_models/key_management/KeyLookup.py +63 -0
- edsl/language_models/key_management/KeyLookupBuilder.py +273 -0
- edsl/language_models/key_management/KeyLookupCollection.py +38 -0
- edsl/language_models/key_management/__init__.py +0 -0
- edsl/language_models/key_management/models.py +131 -0
- edsl/language_models/model.py +256 -0
- edsl/language_models/repair.py +2 -2
- edsl/language_models/utilities.py +5 -4
- edsl/notebooks/Notebook.py +19 -14
- edsl/notebooks/NotebookToLaTeX.py +142 -0
- edsl/prompts/Prompt.py +29 -39
- edsl/questions/ExceptionExplainer.py +77 -0
- edsl/questions/HTMLQuestion.py +103 -0
- edsl/questions/QuestionBase.py +68 -214
- edsl/questions/QuestionBasePromptsMixin.py +7 -3
- edsl/questions/QuestionBudget.py +1 -1
- edsl/questions/QuestionCheckBox.py +3 -3
- edsl/questions/QuestionExtract.py +5 -7
- edsl/questions/QuestionFreeText.py +2 -3
- edsl/questions/QuestionList.py +10 -18
- edsl/questions/QuestionMatrix.py +265 -0
- edsl/questions/QuestionMultipleChoice.py +67 -23
- edsl/questions/QuestionNumerical.py +2 -4
- edsl/questions/QuestionRank.py +7 -17
- edsl/questions/SimpleAskMixin.py +4 -3
- edsl/questions/__init__.py +2 -1
- edsl/questions/{AnswerValidatorMixin.py → answer_validator_mixin.py} +47 -2
- edsl/questions/data_structures.py +20 -0
- edsl/questions/derived/QuestionLinearScale.py +6 -3
- edsl/questions/derived/QuestionTopK.py +1 -1
- edsl/questions/descriptors.py +17 -3
- edsl/questions/loop_processor.py +149 -0
- edsl/questions/{QuestionBaseGenMixin.py → question_base_gen_mixin.py} +57 -50
- edsl/questions/question_registry.py +1 -1
- edsl/questions/{ResponseValidatorABC.py → response_validator_abc.py} +40 -26
- edsl/questions/response_validator_factory.py +34 -0
- edsl/questions/templates/matrix/__init__.py +1 -0
- edsl/questions/templates/matrix/answering_instructions.jinja +5 -0
- edsl/questions/templates/matrix/question_presentation.jinja +20 -0
- edsl/results/CSSParameterizer.py +1 -1
- edsl/results/Dataset.py +170 -7
- edsl/results/DatasetExportMixin.py +168 -305
- edsl/results/DatasetTree.py +28 -8
- edsl/results/MarkdownToDocx.py +122 -0
- edsl/results/MarkdownToPDF.py +111 -0
- edsl/results/Result.py +298 -206
- edsl/results/Results.py +149 -131
- edsl/results/ResultsExportMixin.py +2 -0
- edsl/results/TableDisplay.py +98 -171
- edsl/results/TextEditor.py +50 -0
- edsl/results/__init__.py +1 -1
- edsl/results/file_exports.py +252 -0
- edsl/results/{Selector.py → results_selector.py} +23 -13
- edsl/results/smart_objects.py +96 -0
- edsl/results/table_data_class.py +12 -0
- edsl/results/table_renderers.py +118 -0
- edsl/scenarios/ConstructDownloadLink.py +109 -0
- edsl/scenarios/DocumentChunker.py +102 -0
- edsl/scenarios/DocxScenario.py +16 -0
- edsl/scenarios/FileStore.py +150 -239
- edsl/scenarios/PdfExtractor.py +40 -0
- edsl/scenarios/Scenario.py +90 -193
- edsl/scenarios/ScenarioHtmlMixin.py +4 -3
- edsl/scenarios/ScenarioList.py +415 -244
- edsl/scenarios/ScenarioListExportMixin.py +0 -7
- edsl/scenarios/ScenarioListPdfMixin.py +15 -37
- edsl/scenarios/__init__.py +1 -2
- edsl/scenarios/directory_scanner.py +96 -0
- edsl/scenarios/file_methods.py +85 -0
- edsl/scenarios/handlers/__init__.py +13 -0
- edsl/scenarios/handlers/csv.py +49 -0
- edsl/scenarios/handlers/docx.py +76 -0
- edsl/scenarios/handlers/html.py +37 -0
- edsl/scenarios/handlers/json.py +111 -0
- edsl/scenarios/handlers/latex.py +5 -0
- edsl/scenarios/handlers/md.py +51 -0
- edsl/scenarios/handlers/pdf.py +68 -0
- edsl/scenarios/handlers/png.py +39 -0
- edsl/scenarios/handlers/pptx.py +105 -0
- edsl/scenarios/handlers/py.py +294 -0
- edsl/scenarios/handlers/sql.py +313 -0
- edsl/scenarios/handlers/sqlite.py +149 -0
- edsl/scenarios/handlers/txt.py +33 -0
- edsl/scenarios/{ScenarioJoin.py → scenario_join.py} +10 -6
- edsl/scenarios/scenario_selector.py +156 -0
- edsl/study/ObjectEntry.py +1 -1
- edsl/study/SnapShot.py +1 -1
- edsl/study/Study.py +5 -12
- edsl/surveys/ConstructDAG.py +92 -0
- edsl/surveys/EditSurvey.py +221 -0
- edsl/surveys/InstructionHandler.py +100 -0
- edsl/surveys/MemoryManagement.py +72 -0
- edsl/surveys/Rule.py +5 -4
- edsl/surveys/RuleCollection.py +25 -27
- edsl/surveys/RuleManager.py +172 -0
- edsl/surveys/Simulator.py +75 -0
- edsl/surveys/Survey.py +270 -791
- edsl/surveys/SurveyCSS.py +20 -8
- edsl/surveys/{SurveyFlowVisualizationMixin.py → SurveyFlowVisualization.py} +11 -9
- edsl/surveys/SurveyToApp.py +141 -0
- edsl/surveys/__init__.py +4 -2
- edsl/surveys/descriptors.py +6 -2
- edsl/surveys/instructions/ChangeInstruction.py +1 -2
- edsl/surveys/instructions/Instruction.py +4 -13
- edsl/surveys/instructions/InstructionCollection.py +11 -6
- edsl/templates/error_reporting/interview_details.html +1 -1
- edsl/templates/error_reporting/report.html +1 -1
- edsl/tools/plotting.py +1 -1
- edsl/utilities/PrettyList.py +56 -0
- edsl/utilities/is_notebook.py +18 -0
- edsl/utilities/is_valid_variable_name.py +11 -0
- edsl/utilities/remove_edsl_version.py +24 -0
- edsl/utilities/utilities.py +35 -23
- {edsl-0.1.38.dev4.dist-info → edsl-0.1.39.dist-info}/METADATA +12 -10
- edsl-0.1.39.dist-info/RECORD +358 -0
- {edsl-0.1.38.dev4.dist-info → edsl-0.1.39.dist-info}/WHEEL +1 -1
- edsl/language_models/KeyLookup.py +0 -30
- edsl/language_models/registry.py +0 -190
- edsl/language_models/unused/ReplicateBase.py +0 -83
- edsl/results/ResultsDBMixin.py +0 -238
- edsl-0.1.38.dev4.dist-info/RECORD +0 -277
- /edsl/questions/{RegisterQuestionsMeta.py → register_questions_meta.py} +0 -0
- /edsl/results/{ResultsFetchMixin.py → results_fetch_mixin.py} +0 -0
- /edsl/results/{ResultsToolsMixin.py → results_tools_mixin.py} +0 -0
- {edsl-0.1.38.dev4.dist-info → edsl-0.1.39.dist-info}/LICENSE +0 -0
edsl/study/ObjectEntry.py
CHANGED
edsl/study/SnapShot.py
CHANGED
@@ -32,7 +32,7 @@ class SnapShot:
|
|
32
32
|
{'Cache': <class 'edsl.data.Cache.Cache'>}
|
33
33
|
"""
|
34
34
|
from edsl.Base import RegisterSubclassesMeta
|
35
|
-
from edsl import QuestionBase
|
35
|
+
from edsl.questions.QuestionBase import QuestionBase
|
36
36
|
|
37
37
|
all_edsl_objects = RegisterSubclassesMeta.get_registry()
|
38
38
|
|
edsl/study/Study.py
CHANGED
@@ -7,7 +7,9 @@ import socket
|
|
7
7
|
from datetime import datetime
|
8
8
|
from typing import Dict, Optional, Union
|
9
9
|
from uuid import UUID, uuid4
|
10
|
-
|
10
|
+
|
11
|
+
from edsl.data.Cache import Cache
|
12
|
+
from edsl import set_session_cache, unset_session_cache
|
11
13
|
from edsl.utilities.utilities import dict_hash
|
12
14
|
from edsl.study.ObjectEntry import ObjectEntry
|
13
15
|
from edsl.study.ProofOfWork import ProofOfWork
|
@@ -405,7 +407,7 @@ class Study:
|
|
405
407
|
|
406
408
|
study_file = tempfile.NamedTemporaryFile()
|
407
409
|
with cls(filename=study_file.name, verbose=verbose) as study:
|
408
|
-
from edsl import QuestionFreeText
|
410
|
+
from edsl.questions.QuestionFreeText import QuestionFreeText
|
409
411
|
|
410
412
|
q = QuestionFreeText.example(randomize=randomize)
|
411
413
|
return study
|
@@ -464,7 +466,7 @@ class Study:
|
|
464
466
|
def push(self) -> dict:
|
465
467
|
"""Push the objects to coop."""
|
466
468
|
|
467
|
-
from edsl import Coop
|
469
|
+
from edsl.coop.coop import Coop
|
468
470
|
|
469
471
|
coop = Coop()
|
470
472
|
return coop.create(self, description=self.description)
|
@@ -517,12 +519,3 @@ if __name__ == "__main__":
|
|
517
519
|
import doctest
|
518
520
|
|
519
521
|
doctest.testmod(optionflags=doctest.ELLIPSIS)
|
520
|
-
|
521
|
-
# with Study(name = "cool_study") as study:
|
522
|
-
# from edsl import QuestionFreeText
|
523
|
-
# q = QuestionFreeText.example()
|
524
|
-
|
525
|
-
# assert len(study.objects) == 1
|
526
|
-
|
527
|
-
# print(study.versions())
|
528
|
-
# {'q': [ObjectEntry(variable_name='q', object=Question('free_text', question_name = """how_are_you""", question_text = """How are you?"""), description='Question name: how_are_you', coop_info=None, created_at=1720276402.561273, edsl_class_name='QuestionFreeText')]}
|
@@ -0,0 +1,92 @@
|
|
1
|
+
from edsl.surveys.base import EndOfSurvey
|
2
|
+
from edsl.surveys.DAG import DAG
|
3
|
+
from edsl.exceptions.surveys import SurveyError
|
4
|
+
|
5
|
+
|
6
|
+
class ConstructDAG:
|
7
|
+
def __init__(self, survey):
|
8
|
+
self.survey = survey
|
9
|
+
self.questions = survey.questions
|
10
|
+
|
11
|
+
self.parameters_by_question = self.survey.parameters_by_question
|
12
|
+
self.question_name_to_index = self.survey.question_name_to_index
|
13
|
+
|
14
|
+
def dag(self, textify: bool = False) -> DAG:
|
15
|
+
memory_dag = self.survey.memory_plan.dag
|
16
|
+
rule_dag = self.survey.rule_collection.dag
|
17
|
+
piping_dag = self.piping_dag
|
18
|
+
if textify:
|
19
|
+
memory_dag = DAG(self.textify(memory_dag))
|
20
|
+
rule_dag = DAG(self.textify(rule_dag))
|
21
|
+
piping_dag = DAG(self.textify(piping_dag))
|
22
|
+
return memory_dag + rule_dag + piping_dag
|
23
|
+
|
24
|
+
@property
|
25
|
+
def piping_dag(self) -> DAG:
|
26
|
+
"""Figures out the DAG of piping dependencies.
|
27
|
+
|
28
|
+
>>> from edsl import Survey
|
29
|
+
>>> from edsl import QuestionFreeText
|
30
|
+
>>> q0 = QuestionFreeText(question_text="Here is a question", question_name="q0")
|
31
|
+
>>> q1 = QuestionFreeText(question_text="You previously answered {{ q0 }}---how do you feel now?", question_name="q1")
|
32
|
+
>>> s = Survey([q0, q1])
|
33
|
+
>>> ConstructDAG(s).piping_dag
|
34
|
+
{1: {0}}
|
35
|
+
"""
|
36
|
+
d = {}
|
37
|
+
for question_name, depenencies in self.parameters_by_question.items():
|
38
|
+
if depenencies:
|
39
|
+
question_index = self.question_name_to_index[question_name]
|
40
|
+
for dependency in depenencies:
|
41
|
+
if dependency not in self.question_name_to_index:
|
42
|
+
pass
|
43
|
+
else:
|
44
|
+
dependency_index = self.question_name_to_index[dependency]
|
45
|
+
if question_index not in d:
|
46
|
+
d[question_index] = set()
|
47
|
+
d[question_index].add(dependency_index)
|
48
|
+
return d
|
49
|
+
|
50
|
+
def textify(self, index_dag: DAG) -> DAG:
|
51
|
+
"""Convert the DAG of question indices to a DAG of question names.
|
52
|
+
|
53
|
+
:param index_dag: The DAG of question indices.
|
54
|
+
|
55
|
+
Example:
|
56
|
+
|
57
|
+
>>> from edsl import Survey
|
58
|
+
>>> s = Survey.example()
|
59
|
+
>>> d = s.dag()
|
60
|
+
>>> d
|
61
|
+
{1: {0}, 2: {0}}
|
62
|
+
>>> ConstructDAG(s).textify(d)
|
63
|
+
{'q1': {'q0'}, 'q2': {'q0'}}
|
64
|
+
"""
|
65
|
+
|
66
|
+
def get_name(index: int):
|
67
|
+
"""Return the name of the question given the index."""
|
68
|
+
if index >= len(self.questions):
|
69
|
+
return EndOfSurvey
|
70
|
+
try:
|
71
|
+
return self.questions[index].question_name
|
72
|
+
except IndexError:
|
73
|
+
print(
|
74
|
+
f"The index is {index} but the length of the questions is {len(self.questions)}"
|
75
|
+
)
|
76
|
+
raise SurveyError
|
77
|
+
|
78
|
+
try:
|
79
|
+
text_dag = {}
|
80
|
+
for child_index, parent_indices in index_dag.items():
|
81
|
+
parent_names = {get_name(index) for index in parent_indices}
|
82
|
+
child_name = get_name(child_index)
|
83
|
+
text_dag[child_name] = parent_names
|
84
|
+
return text_dag
|
85
|
+
except IndexError:
|
86
|
+
raise
|
87
|
+
|
88
|
+
|
89
|
+
if __name__ == "__main__":
|
90
|
+
import doctest
|
91
|
+
|
92
|
+
doctest.testmod()
|
@@ -0,0 +1,221 @@
|
|
1
|
+
from typing import Union, Optional, TYPE_CHECKING
|
2
|
+
from edsl.exceptions.surveys import SurveyError
|
3
|
+
|
4
|
+
if TYPE_CHECKING:
|
5
|
+
from edsl.questions.QuestionBase import QuestionBase
|
6
|
+
|
7
|
+
from edsl.exceptions.surveys import SurveyError, SurveyCreationError
|
8
|
+
from edsl.surveys.Rule import Rule
|
9
|
+
from edsl.surveys.base import RulePriority, EndOfSurvey
|
10
|
+
|
11
|
+
|
12
|
+
class EditSurvey:
|
13
|
+
def __init__(self, survey):
|
14
|
+
self.survey = survey
|
15
|
+
|
16
|
+
def move_question(self, identifier: Union[str, int], new_index: int) -> "Survey":
|
17
|
+
if isinstance(identifier, str):
|
18
|
+
if identifier not in self.survey.question_names:
|
19
|
+
raise SurveyError(
|
20
|
+
f"Question name '{identifier}' does not exist in the survey."
|
21
|
+
)
|
22
|
+
index = self.survey.question_name_to_index[identifier]
|
23
|
+
elif isinstance(identifier, int):
|
24
|
+
if identifier < 0 or identifier >= len(self.survey.questions):
|
25
|
+
raise SurveyError(f"Index {identifier} is out of range.")
|
26
|
+
index = identifier
|
27
|
+
else:
|
28
|
+
raise SurveyError(
|
29
|
+
"Identifier must be either a string (question name) or an integer (question index)."
|
30
|
+
)
|
31
|
+
|
32
|
+
moving_question = self.survey._questions[index]
|
33
|
+
|
34
|
+
new_survey = self.survey.delete_question(index)
|
35
|
+
new_survey.add_question(moving_question, new_index)
|
36
|
+
return new_survey
|
37
|
+
|
38
|
+
def add_question(
|
39
|
+
self, question: "QuestionBase", index: Optional[int] = None
|
40
|
+
) -> "Survey":
|
41
|
+
if question.question_name in self.survey.question_names:
|
42
|
+
raise SurveyCreationError(
|
43
|
+
f"""Question name '{question.question_name}' already exists in survey. Existing names are {self.survey.question_names}."""
|
44
|
+
)
|
45
|
+
if index is None:
|
46
|
+
index = len(self.survey.questions)
|
47
|
+
|
48
|
+
if index > len(self.survey.questions):
|
49
|
+
raise SurveyCreationError(
|
50
|
+
f"Index {index} is greater than the number of questions in the survey."
|
51
|
+
)
|
52
|
+
if index < 0:
|
53
|
+
raise SurveyCreationError(f"Index {index} is less than 0.")
|
54
|
+
|
55
|
+
interior_insertion = index != len(self.survey.questions)
|
56
|
+
|
57
|
+
# index = len(self.survey.questions)
|
58
|
+
# TODO: This is a bit ugly because the user
|
59
|
+
# doesn't "know" about _questions - it's generated by the
|
60
|
+
# descriptor.
|
61
|
+
self.survey._questions.insert(index, question)
|
62
|
+
|
63
|
+
if interior_insertion:
|
64
|
+
for question_name, old_index in self.survey._pseudo_indices.items():
|
65
|
+
if old_index >= index:
|
66
|
+
self.survey._pseudo_indices[question_name] = old_index + 1
|
67
|
+
|
68
|
+
self.survey._pseudo_indices[question.question_name] = index
|
69
|
+
|
70
|
+
## Re-do question_name to index - this is done automatically
|
71
|
+
# for question_name, old_index in self.survey.question_name_to_index.items():
|
72
|
+
# if old_index >= index:
|
73
|
+
# self.survey.question_name_to_index[question_name] = old_index + 1
|
74
|
+
|
75
|
+
## Need to re-do the rule collection and the indices of the questions
|
76
|
+
|
77
|
+
## If a rule is before the insertion index and next_q is also before the insertion index, no change needed.
|
78
|
+
## If the rule is before the insertion index but next_q is after the insertion index, increment the next_q by 1
|
79
|
+
## If the rule is after the insertion index, increment the current_q by 1 and the next_q by 1
|
80
|
+
|
81
|
+
# using index + 1 presumes there is a next question
|
82
|
+
if interior_insertion:
|
83
|
+
for rule in self.survey.rule_collection:
|
84
|
+
if rule.current_q >= index:
|
85
|
+
rule.current_q += 1
|
86
|
+
if rule.next_q >= index:
|
87
|
+
rule.next_q += 1
|
88
|
+
|
89
|
+
# add a new rule
|
90
|
+
self.survey.rule_collection.add_rule(
|
91
|
+
Rule(
|
92
|
+
current_q=index,
|
93
|
+
expression="True",
|
94
|
+
next_q=index + 1,
|
95
|
+
question_name_to_index=self.survey.question_name_to_index,
|
96
|
+
priority=RulePriority.DEFAULT.value,
|
97
|
+
)
|
98
|
+
)
|
99
|
+
|
100
|
+
# a question might be added before the memory plan is created
|
101
|
+
# it's ok because the memory plan will be updated when it is created
|
102
|
+
if hasattr(self.survey, "memory_plan"):
|
103
|
+
self.survey.memory_plan.add_question(question)
|
104
|
+
|
105
|
+
return self.survey
|
106
|
+
|
107
|
+
def delete_question(self, identifier: Union[str, int]) -> "Survey":
|
108
|
+
"""
|
109
|
+
Delete a question from the survey.
|
110
|
+
|
111
|
+
:param identifier: The name or index of the question to delete.
|
112
|
+
:return: The updated Survey object.
|
113
|
+
|
114
|
+
>>> from edsl import QuestionMultipleChoice, Survey
|
115
|
+
>>> q1 = QuestionMultipleChoice(question_text="Q1", question_options=["A", "B"], question_name="q1")
|
116
|
+
>>> q2 = QuestionMultipleChoice(question_text="Q2", question_options=["C", "D"], question_name="q2")
|
117
|
+
>>> s = Survey().add_question(q1).add_question(q2)
|
118
|
+
>>> _ = s.delete_question("q1")
|
119
|
+
>>> len(s.questions)
|
120
|
+
1
|
121
|
+
>>> _ = s.delete_question(0)
|
122
|
+
>>> len(s.questions)
|
123
|
+
0
|
124
|
+
"""
|
125
|
+
if isinstance(identifier, str):
|
126
|
+
if identifier not in self.survey.question_names:
|
127
|
+
raise SurveyError(
|
128
|
+
f"Question name '{identifier}' does not exist in the survey."
|
129
|
+
)
|
130
|
+
index = self.survey.question_name_to_index[identifier]
|
131
|
+
elif isinstance(identifier, int):
|
132
|
+
if identifier < 0 or identifier >= len(self.survey.questions):
|
133
|
+
raise SurveyError(f"Index {identifier} is out of range.")
|
134
|
+
index = identifier
|
135
|
+
else:
|
136
|
+
raise SurveyError(
|
137
|
+
"Identifier must be either a string (question name) or an integer (question index)."
|
138
|
+
)
|
139
|
+
|
140
|
+
# Remove the question
|
141
|
+
deleted_question = self.survey._questions.pop(index)
|
142
|
+
del self.survey._pseudo_indices[deleted_question.question_name]
|
143
|
+
|
144
|
+
# Update indices
|
145
|
+
for question_name, old_index in self.survey._pseudo_indices.items():
|
146
|
+
if old_index > index:
|
147
|
+
self.survey._pseudo_indices[question_name] = old_index - 1
|
148
|
+
|
149
|
+
# Update rules
|
150
|
+
from .RuleCollection import RuleCollection
|
151
|
+
|
152
|
+
new_rule_collection = RuleCollection()
|
153
|
+
for rule in self.survey.rule_collection:
|
154
|
+
if rule.current_q == index:
|
155
|
+
continue # Remove rules associated with the deleted question
|
156
|
+
if rule.current_q > index:
|
157
|
+
rule.current_q -= 1
|
158
|
+
if rule.next_q > index:
|
159
|
+
rule.next_q -= 1
|
160
|
+
|
161
|
+
if rule.next_q == index:
|
162
|
+
if index == len(self.survey.questions):
|
163
|
+
rule.next_q = EndOfSurvey
|
164
|
+
else:
|
165
|
+
rule.next_q = index
|
166
|
+
|
167
|
+
new_rule_collection.add_rule(rule)
|
168
|
+
self.survey.rule_collection = new_rule_collection
|
169
|
+
|
170
|
+
# Update memory plan if it exists
|
171
|
+
if hasattr(self.survey, "memory_plan"):
|
172
|
+
self.survey.memory_plan.remove_question(deleted_question.question_name)
|
173
|
+
|
174
|
+
return self.survey
|
175
|
+
|
176
|
+
def add_instruction(
|
177
|
+
self, instruction: Union["Instruction", "ChangeInstruction"]
|
178
|
+
) -> "Survey":
|
179
|
+
"""
|
180
|
+
Add an instruction to the survey.
|
181
|
+
|
182
|
+
:param instruction: The instruction to add to the survey.
|
183
|
+
|
184
|
+
>>> from edsl import Instruction
|
185
|
+
>>> from edsl.surveys.Survey import Survey
|
186
|
+
>>> i = Instruction(text="Pay attention to the following questions.", name="intro")
|
187
|
+
>>> s = Survey().add_instruction(i)
|
188
|
+
>>> s._instruction_names_to_instructions
|
189
|
+
{'intro': Instruction(name="intro", text="Pay attention to the following questions.")}
|
190
|
+
>>> s._pseudo_indices
|
191
|
+
{'intro': -0.5}
|
192
|
+
"""
|
193
|
+
import math
|
194
|
+
|
195
|
+
if instruction.name in self.survey._instruction_names_to_instructions:
|
196
|
+
raise SurveyCreationError(
|
197
|
+
f"""Instruction name '{instruction.name}' already exists in survey. Existing names are {self.survey._instruction_names_to_instructions.keys()}."""
|
198
|
+
)
|
199
|
+
self.survey._instruction_names_to_instructions[instruction.name] = instruction
|
200
|
+
|
201
|
+
# was the last thing added an instruction or a question?
|
202
|
+
if self.survey._pseudo_indices.last_item_was_instruction:
|
203
|
+
pseudo_index = (
|
204
|
+
self.survey._pseudo_indices.max_pseudo_index
|
205
|
+
+ (
|
206
|
+
math.ceil(self.survey._pseudo_indices.max_pseudo_index)
|
207
|
+
- self.survey._pseudo_indices.max_pseudo_index
|
208
|
+
)
|
209
|
+
/ 2
|
210
|
+
)
|
211
|
+
else:
|
212
|
+
pseudo_index = self.survey._pseudo_indices.max_pseudo_index + 1.0 / 2.0
|
213
|
+
self.survey._pseudo_indices[instruction.name] = pseudo_index
|
214
|
+
|
215
|
+
return self.survey
|
216
|
+
|
217
|
+
|
218
|
+
if __name__ == "__main__":
|
219
|
+
import doctest
|
220
|
+
|
221
|
+
doctest.testmod()
|
@@ -0,0 +1,100 @@
|
|
1
|
+
from typing import TYPE_CHECKING
|
2
|
+
from dataclasses import dataclass
|
3
|
+
|
4
|
+
|
5
|
+
@dataclass
|
6
|
+
class SeparatedComponents:
|
7
|
+
true_questions: list
|
8
|
+
instruction_names_to_instructions: dict
|
9
|
+
pseudo_indices: dict
|
10
|
+
|
11
|
+
|
12
|
+
class InstructionHandler:
|
13
|
+
def __init__(self, survey):
|
14
|
+
self.survey = survey
|
15
|
+
|
16
|
+
@staticmethod
|
17
|
+
def separate_questions_and_instructions(questions_and_instructions: list) -> tuple:
|
18
|
+
"""
|
19
|
+
The 'pseudo_indices' attribute is a dictionary that maps question names to pseudo-indices
|
20
|
+
that are used to order questions and instructions in the survey.
|
21
|
+
Only questions get real indices; instructions get pseudo-indices.
|
22
|
+
However, the order of the pseudo-indices is the same as the order questions and instructions are added to the survey.
|
23
|
+
|
24
|
+
We don't have to know how many instructions there are to calculate the pseudo-indices because they are
|
25
|
+
calculated by the inverse of one minus the sum of 1/2^n for n in the number of instructions run so far.
|
26
|
+
|
27
|
+
>>> from edsl import Survey
|
28
|
+
>>> from edsl import Instruction
|
29
|
+
>>> i = Instruction(text = "Pay attention to the following questions.", name = "intro")
|
30
|
+
>>> i2 = Instruction(text = "How are you feeling today?", name = "followon_intro")
|
31
|
+
>>> from edsl import QuestionFreeText; q1 = QuestionFreeText.example()
|
32
|
+
>>> from edsl import QuestionMultipleChoice; q2 = QuestionMultipleChoice.example()
|
33
|
+
>>> s = Survey([q1, i, i2, q2])
|
34
|
+
>>> len(s._instruction_names_to_instructions)
|
35
|
+
2
|
36
|
+
>>> s._pseudo_indices
|
37
|
+
{'how_are_you': 0, 'intro': 0.5, 'followon_intro': 0.75, 'how_feeling': 1}
|
38
|
+
|
39
|
+
>>> from edsl import ChangeInstruction
|
40
|
+
>>> q3 = QuestionFreeText(question_text = "What is your favorite color?", question_name = "color")
|
41
|
+
>>> i_change = ChangeInstruction(drop = ["intro"])
|
42
|
+
>>> s = Survey([q1, i, q2, i_change, q3])
|
43
|
+
>>> [i.name for i in s._relevant_instructions(q1)]
|
44
|
+
[]
|
45
|
+
>>> [i.name for i in s._relevant_instructions(q2)]
|
46
|
+
['intro']
|
47
|
+
>>> [i.name for i in s._relevant_instructions(q3)]
|
48
|
+
[]
|
49
|
+
|
50
|
+
>>> i_change = ChangeInstruction(keep = ["poop"], drop = [])
|
51
|
+
>>> s = Survey([q1, i, q2, i_change])
|
52
|
+
Traceback (most recent call last):
|
53
|
+
...
|
54
|
+
ValueError: ChangeInstruction change_instruction_0 references instruction poop which does not exist.
|
55
|
+
"""
|
56
|
+
from edsl.surveys.instructions.Instruction import Instruction
|
57
|
+
from edsl.surveys.instructions.ChangeInstruction import ChangeInstruction
|
58
|
+
from edsl.questions.QuestionBase import QuestionBase
|
59
|
+
|
60
|
+
true_questions = []
|
61
|
+
instruction_names_to_instructions = {}
|
62
|
+
|
63
|
+
num_change_instructions = 0
|
64
|
+
pseudo_indices = {}
|
65
|
+
instructions_run_length = 0
|
66
|
+
for entry in questions_and_instructions:
|
67
|
+
if isinstance(entry, Instruction) or isinstance(entry, ChangeInstruction):
|
68
|
+
if isinstance(entry, ChangeInstruction):
|
69
|
+
entry.add_name(num_change_instructions)
|
70
|
+
num_change_instructions += 1
|
71
|
+
for prior_instruction in entry.keep + entry.drop:
|
72
|
+
if prior_instruction not in instruction_names_to_instructions:
|
73
|
+
raise ValueError(
|
74
|
+
f"ChangeInstruction {entry.name} references instruction {prior_instruction} which does not exist."
|
75
|
+
)
|
76
|
+
instructions_run_length += 1
|
77
|
+
delta = 1 - 1.0 / (2.0**instructions_run_length)
|
78
|
+
pseudo_index = (len(true_questions) - 1) + delta
|
79
|
+
entry.pseudo_index = pseudo_index
|
80
|
+
instruction_names_to_instructions[entry.name] = entry
|
81
|
+
elif isinstance(entry, QuestionBase):
|
82
|
+
pseudo_index = len(true_questions)
|
83
|
+
instructions_run_length = 0
|
84
|
+
true_questions.append(entry)
|
85
|
+
else:
|
86
|
+
raise ValueError(
|
87
|
+
f"Entry {repr(entry)} is not a QuestionBase or an Instruction."
|
88
|
+
)
|
89
|
+
|
90
|
+
pseudo_indices[entry.name] = pseudo_index
|
91
|
+
|
92
|
+
return SeparatedComponents(
|
93
|
+
true_questions, instruction_names_to_instructions, pseudo_indices
|
94
|
+
)
|
95
|
+
|
96
|
+
|
97
|
+
if __name__ == "__main__":
|
98
|
+
import doctest
|
99
|
+
|
100
|
+
doctest.testmod()
|
@@ -0,0 +1,72 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
from typing import Callable, Union, List, TYPE_CHECKING
|
3
|
+
|
4
|
+
if TYPE_CHECKING:
|
5
|
+
from edsl.questions.QuestionBase import QuestionBase
|
6
|
+
|
7
|
+
|
8
|
+
class MemoryManagement:
|
9
|
+
def __init__(self, survey):
|
10
|
+
self.survey = survey
|
11
|
+
|
12
|
+
def _set_memory_plan(self, prior_questions_func: Callable) -> None:
|
13
|
+
"""Set memory plan based on a provided function determining prior questions.
|
14
|
+
:param prior_questions_func: A function that takes an index and returns a list of prior questions.
|
15
|
+
"""
|
16
|
+
for i, question_name in enumerate(self.survey.question_names):
|
17
|
+
self.survey.memory_plan.add_memory_collection(
|
18
|
+
focal_question=question_name,
|
19
|
+
prior_questions=prior_questions_func(i),
|
20
|
+
)
|
21
|
+
|
22
|
+
def add_targeted_memory(
|
23
|
+
self,
|
24
|
+
focal_question: Union[QuestionBase, str],
|
25
|
+
prior_question: Union[QuestionBase, str],
|
26
|
+
) -> "Survey":
|
27
|
+
"""Add instructions to a survey than when answering focal_question.
|
28
|
+
|
29
|
+
:param focal_question: The question that the agent is answering.
|
30
|
+
:param prior_question: The question that the agent should remember when answering the focal question.
|
31
|
+
|
32
|
+
Here we add instructions to a survey than when answering q2 they should remember q1:
|
33
|
+
"""
|
34
|
+
focal_question_name = self.survey.question_names[
|
35
|
+
self.survey._get_question_index(focal_question)
|
36
|
+
]
|
37
|
+
prior_question_name = self.survey.question_names[
|
38
|
+
self.survey._get_question_index(prior_question)
|
39
|
+
]
|
40
|
+
|
41
|
+
self.survey.memory_plan.add_single_memory(
|
42
|
+
focal_question=focal_question_name,
|
43
|
+
prior_question=prior_question_name,
|
44
|
+
)
|
45
|
+
|
46
|
+
return self.survey
|
47
|
+
|
48
|
+
def add_memory_collection(
|
49
|
+
self,
|
50
|
+
focal_question: Union[QuestionBase, str],
|
51
|
+
prior_questions: List[Union[QuestionBase, str]],
|
52
|
+
) -> "Survey":
|
53
|
+
"""Add prior questions and responses so the agent has them when answering.
|
54
|
+
|
55
|
+
This adds instructions to a survey than when answering focal_question, the agent should also remember the answers to prior_questions listed in prior_questions.
|
56
|
+
|
57
|
+
:param focal_question: The question that the agent is answering.
|
58
|
+
:param prior_questions: The questions that the agent should remember when answering the focal question.
|
59
|
+
"""
|
60
|
+
focal_question_name = self.survey.question_names[
|
61
|
+
self.survey._get_question_index(focal_question)
|
62
|
+
]
|
63
|
+
|
64
|
+
prior_question_names = [
|
65
|
+
self.survey.question_names[self.survey._get_question_index(prior_question)]
|
66
|
+
for prior_question in prior_questions
|
67
|
+
]
|
68
|
+
|
69
|
+
self.survey.memory_plan.add_memory_collection(
|
70
|
+
focal_question=focal_question_name, prior_questions=prior_question_names
|
71
|
+
)
|
72
|
+
return self.survey
|
edsl/surveys/Rule.py
CHANGED
@@ -21,13 +21,13 @@ import ast
|
|
21
21
|
import random
|
22
22
|
from typing import Any, Union, List
|
23
23
|
|
24
|
-
|
25
|
-
from rich import print
|
24
|
+
|
25
|
+
# from rich import print
|
26
26
|
from simpleeval import EvalWithCompoundTypes
|
27
27
|
|
28
28
|
from edsl.exceptions.surveys import SurveyError
|
29
29
|
|
30
|
-
from edsl.exceptions import (
|
30
|
+
from edsl.exceptions.surveys import (
|
31
31
|
SurveyRuleCannotEvaluateError,
|
32
32
|
SurveyRuleCollectionHasNoRulesAtNodeError,
|
33
33
|
SurveyRuleRefersToFutureStateError,
|
@@ -37,7 +37,7 @@ from edsl.exceptions import (
|
|
37
37
|
)
|
38
38
|
from edsl.surveys.base import EndOfSurvey
|
39
39
|
from edsl.utilities.ast_utilities import extract_variable_names
|
40
|
-
from edsl.utilities.
|
40
|
+
from edsl.utilities.remove_edsl_version import remove_edsl_version
|
41
41
|
|
42
42
|
|
43
43
|
class QuestionIndex:
|
@@ -253,6 +253,7 @@ class Rule:
|
|
253
253
|
...
|
254
254
|
edsl.exceptions.surveys.SurveyRuleCannotEvaluateError...
|
255
255
|
"""
|
256
|
+
from jinja2 import Template
|
256
257
|
|
257
258
|
def substitute_in_answers(expression, current_info_env):
|
258
259
|
"""Take the dictionary of answers and substitute them into the expression."""
|