edsl 0.1.46__py3-none-any.whl → 0.1.48__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/__init__.py +44 -39
- edsl/__version__.py +1 -1
- edsl/agents/__init__.py +4 -2
- edsl/agents/{Agent.py → agent.py} +442 -152
- edsl/agents/{AgentList.py → agent_list.py} +220 -162
- edsl/agents/descriptors.py +46 -7
- edsl/{exceptions/agents.py → agents/exceptions.py} +3 -12
- edsl/base/__init__.py +75 -0
- edsl/base/base_class.py +1303 -0
- edsl/base/data_transfer_models.py +114 -0
- edsl/base/enums.py +215 -0
- edsl/base.py +8 -0
- edsl/buckets/__init__.py +25 -0
- edsl/buckets/bucket_collection.py +324 -0
- edsl/buckets/model_buckets.py +206 -0
- edsl/buckets/token_bucket.py +502 -0
- edsl/{jobs/buckets/TokenBucketAPI.py → buckets/token_bucket_api.py} +1 -1
- edsl/buckets/token_bucket_client.py +509 -0
- edsl/caching/__init__.py +20 -0
- edsl/caching/cache.py +814 -0
- edsl/caching/cache_entry.py +427 -0
- edsl/{data/CacheHandler.py → caching/cache_handler.py} +14 -15
- edsl/caching/exceptions.py +24 -0
- edsl/caching/orm.py +30 -0
- edsl/{data/RemoteCacheSync.py → caching/remote_cache_sync.py} +3 -3
- edsl/caching/sql_dict.py +441 -0
- edsl/config/__init__.py +8 -0
- edsl/config/config_class.py +177 -0
- edsl/config.py +4 -176
- edsl/conversation/Conversation.py +7 -7
- edsl/conversation/car_buying.py +4 -4
- edsl/conversation/chips.py +6 -6
- edsl/coop/__init__.py +25 -2
- edsl/coop/coop.py +430 -113
- edsl/coop/{ExpectedParrotKeyHandler.py → ep_key_handling.py} +86 -10
- edsl/coop/exceptions.py +62 -0
- edsl/coop/price_fetcher.py +126 -0
- edsl/coop/utils.py +89 -24
- edsl/data_transfer_models.py +5 -72
- edsl/dataset/__init__.py +10 -0
- edsl/{results/Dataset.py → dataset/dataset.py} +116 -36
- edsl/dataset/dataset_operations_mixin.py +1492 -0
- edsl/{results/DatasetTree.py → dataset/dataset_tree.py} +156 -75
- edsl/{results/TableDisplay.py → dataset/display/table_display.py} +18 -7
- edsl/{results → dataset/display}/table_renderers.py +58 -2
- edsl/{results → dataset}/file_exports.py +4 -5
- edsl/{results → dataset}/smart_objects.py +2 -2
- edsl/enums.py +5 -205
- edsl/inference_services/__init__.py +5 -0
- edsl/inference_services/{AvailableModelCacheHandler.py → available_model_cache_handler.py} +2 -3
- edsl/inference_services/{AvailableModelFetcher.py → available_model_fetcher.py} +8 -14
- edsl/inference_services/data_structures.py +3 -2
- edsl/{exceptions/inference_services.py → inference_services/exceptions.py} +1 -1
- edsl/inference_services/{InferenceServiceABC.py → inference_service_abc.py} +1 -1
- edsl/inference_services/{InferenceServicesCollection.py → inference_services_collection.py} +8 -7
- edsl/inference_services/registry.py +4 -41
- edsl/inference_services/{ServiceAvailability.py → service_availability.py} +5 -25
- edsl/inference_services/services/__init__.py +31 -0
- edsl/inference_services/{AnthropicService.py → services/anthropic_service.py} +3 -3
- edsl/inference_services/{AwsBedrock.py → services/aws_bedrock.py} +2 -2
- edsl/inference_services/{AzureAI.py → services/azure_ai.py} +2 -2
- edsl/inference_services/{DeepInfraService.py → services/deep_infra_service.py} +1 -3
- edsl/inference_services/{DeepSeekService.py → services/deep_seek_service.py} +2 -4
- edsl/inference_services/{GoogleService.py → services/google_service.py} +5 -4
- edsl/inference_services/{GroqService.py → services/groq_service.py} +1 -1
- edsl/inference_services/{MistralAIService.py → services/mistral_ai_service.py} +3 -3
- edsl/inference_services/{OllamaService.py → services/ollama_service.py} +1 -7
- edsl/inference_services/{OpenAIService.py → services/open_ai_service.py} +5 -6
- edsl/inference_services/{PerplexityService.py → services/perplexity_service.py} +12 -12
- edsl/inference_services/{TestService.py → services/test_service.py} +7 -6
- edsl/inference_services/{TogetherAIService.py → services/together_ai_service.py} +2 -6
- edsl/inference_services/{XAIService.py → services/xai_service.py} +1 -1
- edsl/inference_services/write_available.py +1 -2
- edsl/instructions/__init__.py +6 -0
- edsl/{surveys/instructions/Instruction.py → instructions/instruction.py} +11 -6
- edsl/{surveys/instructions/InstructionCollection.py → instructions/instruction_collection.py} +10 -5
- edsl/{surveys/InstructionHandler.py → instructions/instruction_handler.py} +3 -3
- edsl/{jobs/interviews → interviews}/ReportErrors.py +2 -2
- edsl/interviews/__init__.py +4 -0
- edsl/{jobs/AnswerQuestionFunctionConstructor.py → interviews/answering_function.py} +45 -18
- edsl/{jobs/interviews/InterviewExceptionEntry.py → interviews/exception_tracking.py} +107 -22
- edsl/interviews/interview.py +638 -0
- edsl/{jobs/interviews/InterviewStatusDictionary.py → interviews/interview_status_dictionary.py} +21 -12
- edsl/{jobs/interviews/InterviewStatusLog.py → interviews/interview_status_log.py} +16 -7
- edsl/{jobs/InterviewTaskManager.py → interviews/interview_task_manager.py} +12 -7
- edsl/{jobs/RequestTokenEstimator.py → interviews/request_token_estimator.py} +8 -3
- edsl/{jobs/interviews/InterviewStatistic.py → interviews/statistics.py} +36 -10
- edsl/invigilators/__init__.py +38 -0
- edsl/invigilators/invigilator_base.py +477 -0
- edsl/{agents/Invigilator.py → invigilators/invigilators.py} +263 -10
- edsl/invigilators/prompt_constructor.py +476 -0
- edsl/{agents → invigilators}/prompt_helpers.py +2 -1
- edsl/{agents/QuestionInstructionPromptBuilder.py → invigilators/question_instructions_prompt_builder.py} +18 -13
- edsl/{agents → invigilators}/question_option_processor.py +96 -21
- edsl/{agents/QuestionTemplateReplacementsBuilder.py → invigilators/question_template_replacements_builder.py} +64 -12
- edsl/jobs/__init__.py +7 -1
- edsl/jobs/async_interview_runner.py +99 -35
- edsl/jobs/check_survey_scenario_compatibility.py +7 -5
- edsl/jobs/data_structures.py +153 -22
- edsl/{exceptions/jobs.py → jobs/exceptions.py} +2 -1
- edsl/jobs/{FetchInvigilator.py → fetch_invigilator.py} +4 -4
- edsl/jobs/{loggers/HTMLTableJobLogger.py → html_table_job_logger.py} +6 -2
- edsl/jobs/{Jobs.py → jobs.py} +321 -155
- edsl/jobs/{JobsChecks.py → jobs_checks.py} +15 -7
- edsl/jobs/{JobsComponentConstructor.py → jobs_component_constructor.py} +20 -17
- edsl/jobs/{InterviewsConstructor.py → jobs_interview_constructor.py} +10 -5
- edsl/jobs/jobs_pricing_estimation.py +347 -0
- edsl/jobs/{JobsRemoteInferenceLogger.py → jobs_remote_inference_logger.py} +4 -3
- edsl/jobs/jobs_runner_asyncio.py +282 -0
- edsl/jobs/{JobsRemoteInferenceHandler.py → remote_inference.py} +19 -22
- edsl/jobs/results_exceptions_handler.py +2 -2
- edsl/key_management/__init__.py +28 -0
- edsl/key_management/key_lookup.py +161 -0
- edsl/{language_models/key_management/KeyLookupBuilder.py → key_management/key_lookup_builder.py} +118 -47
- edsl/key_management/key_lookup_collection.py +82 -0
- edsl/key_management/models.py +218 -0
- edsl/language_models/__init__.py +7 -2
- edsl/language_models/{ComputeCost.py → compute_cost.py} +18 -3
- edsl/{exceptions/language_models.py → language_models/exceptions.py} +2 -1
- edsl/language_models/language_model.py +1080 -0
- edsl/language_models/model.py +10 -25
- edsl/language_models/{ModelList.py → model_list.py} +9 -14
- edsl/language_models/{RawResponseHandler.py → raw_response_handler.py} +1 -1
- edsl/language_models/{RegisterLanguageModelsMeta.py → registry.py} +1 -1
- edsl/language_models/repair.py +4 -4
- edsl/language_models/utilities.py +4 -4
- edsl/notebooks/__init__.py +3 -1
- edsl/notebooks/{Notebook.py → notebook.py} +7 -8
- edsl/prompts/__init__.py +1 -1
- edsl/{exceptions/prompts.py → prompts/exceptions.py} +3 -1
- edsl/prompts/{Prompt.py → prompt.py} +101 -95
- edsl/questions/HTMLQuestion.py +1 -1
- edsl/questions/__init__.py +154 -25
- edsl/questions/answer_validator_mixin.py +1 -1
- edsl/questions/compose_questions.py +4 -3
- edsl/questions/derived/question_likert_five.py +166 -0
- edsl/questions/derived/{QuestionLinearScale.py → question_linear_scale.py} +4 -4
- edsl/questions/derived/{QuestionTopK.py → question_top_k.py} +4 -4
- edsl/questions/derived/{QuestionYesNo.py → question_yes_no.py} +4 -5
- edsl/questions/descriptors.py +24 -30
- edsl/questions/loop_processor.py +65 -19
- edsl/questions/question_base.py +881 -0
- edsl/questions/question_base_gen_mixin.py +15 -16
- edsl/questions/{QuestionBasePromptsMixin.py → question_base_prompts_mixin.py} +2 -2
- edsl/questions/{QuestionBudget.py → question_budget.py} +3 -4
- edsl/questions/{QuestionCheckBox.py → question_check_box.py} +16 -16
- edsl/questions/{QuestionDict.py → question_dict.py} +39 -5
- edsl/questions/{QuestionExtract.py → question_extract.py} +9 -9
- edsl/questions/question_free_text.py +282 -0
- edsl/questions/{QuestionFunctional.py → question_functional.py} +6 -5
- edsl/questions/{QuestionList.py → question_list.py} +6 -7
- edsl/questions/{QuestionMatrix.py → question_matrix.py} +6 -5
- edsl/questions/{QuestionMultipleChoice.py → question_multiple_choice.py} +126 -21
- edsl/questions/{QuestionNumerical.py → question_numerical.py} +5 -5
- edsl/questions/{QuestionRank.py → question_rank.py} +6 -6
- edsl/questions/question_registry.py +10 -16
- edsl/questions/register_questions_meta.py +8 -4
- edsl/questions/response_validator_abc.py +17 -16
- edsl/results/__init__.py +4 -1
- edsl/{exceptions/results.py → results/exceptions.py} +1 -1
- edsl/results/report.py +197 -0
- edsl/results/{Result.py → result.py} +131 -45
- edsl/results/{Results.py → results.py} +420 -216
- edsl/results/results_selector.py +344 -25
- edsl/scenarios/__init__.py +30 -3
- edsl/scenarios/{ConstructDownloadLink.py → construct_download_link.py} +7 -0
- edsl/scenarios/directory_scanner.py +156 -13
- edsl/scenarios/document_chunker.py +186 -0
- edsl/scenarios/exceptions.py +101 -0
- edsl/scenarios/file_methods.py +2 -3
- edsl/scenarios/file_store.py +755 -0
- edsl/scenarios/handlers/__init__.py +14 -14
- edsl/scenarios/handlers/{csv.py → csv_file_store.py} +1 -2
- edsl/scenarios/handlers/{docx.py → docx_file_store.py} +8 -7
- edsl/scenarios/handlers/{html.py → html_file_store.py} +1 -2
- edsl/scenarios/handlers/{jpeg.py → jpeg_file_store.py} +1 -1
- edsl/scenarios/handlers/{json.py → json_file_store.py} +1 -1
- edsl/scenarios/handlers/latex_file_store.py +5 -0
- edsl/scenarios/handlers/{md.py → md_file_store.py} +1 -1
- edsl/scenarios/handlers/{pdf.py → pdf_file_store.py} +2 -2
- edsl/scenarios/handlers/{png.py → png_file_store.py} +1 -1
- edsl/scenarios/handlers/{pptx.py → pptx_file_store.py} +8 -7
- edsl/scenarios/handlers/{py.py → py_file_store.py} +1 -3
- edsl/scenarios/handlers/{sql.py → sql_file_store.py} +2 -1
- edsl/scenarios/handlers/{sqlite.py → sqlite_file_store.py} +2 -3
- edsl/scenarios/handlers/{txt.py → txt_file_store.py} +1 -1
- edsl/scenarios/scenario.py +928 -0
- edsl/scenarios/scenario_join.py +18 -5
- edsl/scenarios/{ScenarioList.py → scenario_list.py} +424 -106
- edsl/scenarios/{ScenarioListPdfMixin.py → scenario_list_pdf_tools.py} +16 -15
- edsl/scenarios/scenario_selector.py +5 -1
- edsl/study/ObjectEntry.py +2 -2
- edsl/study/SnapShot.py +5 -5
- edsl/study/Study.py +20 -21
- edsl/study/__init__.py +6 -4
- edsl/surveys/__init__.py +7 -4
- edsl/surveys/dag/__init__.py +2 -0
- edsl/surveys/{ConstructDAG.py → dag/construct_dag.py} +3 -3
- edsl/surveys/{DAG.py → dag/dag.py} +13 -10
- edsl/surveys/descriptors.py +1 -1
- edsl/surveys/{EditSurvey.py → edit_survey.py} +9 -9
- edsl/{exceptions/surveys.py → surveys/exceptions.py} +1 -2
- edsl/surveys/memory/__init__.py +3 -0
- edsl/surveys/{MemoryPlan.py → memory/memory_plan.py} +10 -9
- edsl/surveys/rules/__init__.py +3 -0
- edsl/surveys/{Rule.py → rules/rule.py} +103 -43
- edsl/surveys/{RuleCollection.py → rules/rule_collection.py} +21 -30
- edsl/surveys/{RuleManager.py → rules/rule_manager.py} +19 -13
- edsl/surveys/survey.py +1743 -0
- edsl/surveys/{SurveyExportMixin.py → survey_export.py} +22 -27
- edsl/surveys/{SurveyFlowVisualization.py → survey_flow_visualization.py} +11 -2
- edsl/surveys/{Simulator.py → survey_simulator.py} +10 -3
- edsl/tasks/__init__.py +32 -0
- edsl/{jobs/tasks/QuestionTaskCreator.py → tasks/question_task_creator.py} +115 -57
- edsl/tasks/task_creators.py +135 -0
- edsl/{jobs/tasks/TaskHistory.py → tasks/task_history.py} +86 -47
- edsl/{jobs/tasks → tasks}/task_status_enum.py +91 -7
- edsl/tasks/task_status_log.py +85 -0
- edsl/tokens/__init__.py +2 -0
- edsl/tokens/interview_token_usage.py +53 -0
- edsl/utilities/PrettyList.py +1 -1
- edsl/utilities/SystemInfo.py +25 -22
- edsl/utilities/__init__.py +29 -21
- edsl/utilities/gcp_bucket/__init__.py +2 -0
- edsl/utilities/gcp_bucket/cloud_storage.py +99 -96
- edsl/utilities/interface.py +44 -536
- edsl/{results/MarkdownToPDF.py → utilities/markdown_to_pdf.py} +13 -5
- edsl/utilities/repair_functions.py +1 -1
- {edsl-0.1.46.dist-info → edsl-0.1.48.dist-info}/METADATA +3 -2
- edsl-0.1.48.dist-info/RECORD +347 -0
- edsl/Base.py +0 -426
- edsl/BaseDiff.py +0 -260
- edsl/agents/InvigilatorBase.py +0 -260
- edsl/agents/PromptConstructor.py +0 -318
- edsl/auto/AutoStudy.py +0 -130
- edsl/auto/StageBase.py +0 -243
- edsl/auto/StageGenerateSurvey.py +0 -178
- edsl/auto/StageLabelQuestions.py +0 -125
- edsl/auto/StagePersona.py +0 -61
- edsl/auto/StagePersonaDimensionValueRanges.py +0 -88
- edsl/auto/StagePersonaDimensionValues.py +0 -74
- edsl/auto/StagePersonaDimensions.py +0 -69
- edsl/auto/StageQuestions.py +0 -74
- edsl/auto/SurveyCreatorPipeline.py +0 -21
- edsl/auto/utilities.py +0 -218
- edsl/base/Base.py +0 -279
- edsl/coop/PriceFetcher.py +0 -54
- edsl/data/Cache.py +0 -580
- edsl/data/CacheEntry.py +0 -230
- edsl/data/SQLiteDict.py +0 -292
- edsl/data/__init__.py +0 -5
- edsl/data/orm.py +0 -10
- edsl/exceptions/cache.py +0 -5
- edsl/exceptions/coop.py +0 -14
- edsl/exceptions/data.py +0 -14
- edsl/exceptions/scenarios.py +0 -29
- edsl/jobs/Answers.py +0 -43
- edsl/jobs/JobsPrompts.py +0 -354
- edsl/jobs/buckets/BucketCollection.py +0 -134
- edsl/jobs/buckets/ModelBuckets.py +0 -65
- edsl/jobs/buckets/TokenBucket.py +0 -283
- edsl/jobs/buckets/TokenBucketClient.py +0 -191
- edsl/jobs/interviews/Interview.py +0 -395
- edsl/jobs/interviews/InterviewExceptionCollection.py +0 -99
- edsl/jobs/interviews/InterviewStatisticsCollection.py +0 -25
- edsl/jobs/runners/JobsRunnerAsyncio.py +0 -163
- edsl/jobs/runners/JobsRunnerStatusData.py +0 -0
- edsl/jobs/tasks/TaskCreators.py +0 -64
- edsl/jobs/tasks/TaskStatusLog.py +0 -23
- edsl/jobs/tokens/InterviewTokenUsage.py +0 -27
- edsl/language_models/LanguageModel.py +0 -635
- edsl/language_models/ServiceDataSources.py +0 -0
- edsl/language_models/key_management/KeyLookup.py +0 -63
- edsl/language_models/key_management/KeyLookupCollection.py +0 -38
- edsl/language_models/key_management/models.py +0 -137
- edsl/questions/QuestionBase.py +0 -539
- edsl/questions/QuestionFreeText.py +0 -130
- edsl/questions/derived/QuestionLikertFive.py +0 -76
- edsl/results/DatasetExportMixin.py +0 -911
- edsl/results/ResultsExportMixin.py +0 -45
- edsl/results/TextEditor.py +0 -50
- edsl/results/results_fetch_mixin.py +0 -33
- edsl/results/results_tools_mixin.py +0 -98
- edsl/scenarios/DocumentChunker.py +0 -104
- edsl/scenarios/FileStore.py +0 -564
- edsl/scenarios/Scenario.py +0 -548
- edsl/scenarios/ScenarioHtmlMixin.py +0 -65
- edsl/scenarios/ScenarioListExportMixin.py +0 -45
- edsl/scenarios/handlers/latex.py +0 -5
- edsl/shared.py +0 -1
- edsl/surveys/Survey.py +0 -1306
- edsl/surveys/SurveyQualtricsImport.py +0 -284
- edsl/surveys/SurveyToApp.py +0 -141
- edsl/surveys/instructions/__init__.py +0 -0
- edsl/tools/__init__.py +0 -1
- edsl/tools/clusters.py +0 -192
- edsl/tools/embeddings.py +0 -27
- edsl/tools/embeddings_plotting.py +0 -118
- edsl/tools/plotting.py +0 -112
- edsl/tools/summarize.py +0 -18
- edsl/utilities/data/Registry.py +0 -6
- edsl/utilities/data/__init__.py +0 -1
- edsl/utilities/data/scooter_results.json +0 -1
- edsl-0.1.46.dist-info/RECORD +0 -366
- /edsl/coop/{CoopFunctionsMixin.py → coop_functions.py} +0 -0
- /edsl/{results → dataset/display}/CSSParameterizer.py +0 -0
- /edsl/{language_models/key_management → dataset/display}/__init__.py +0 -0
- /edsl/{results → dataset/display}/table_data_class.py +0 -0
- /edsl/{results → dataset/display}/table_display.css +0 -0
- /edsl/{results/ResultsGGMixin.py → dataset/r/ggplot.py} +0 -0
- /edsl/{results → dataset}/tree_explore.py +0 -0
- /edsl/{surveys/instructions/ChangeInstruction.py → instructions/change_instruction.py} +0 -0
- /edsl/{jobs/interviews → interviews}/interview_status_enum.py +0 -0
- /edsl/jobs/{runners/JobsRunnerStatus.py → jobs_runner_status.py} +0 -0
- /edsl/language_models/{PriceManager.py → price_manager.py} +0 -0
- /edsl/language_models/{fake_openai_call.py → unused/fake_openai_call.py} +0 -0
- /edsl/language_models/{fake_openai_service.py → unused/fake_openai_service.py} +0 -0
- /edsl/notebooks/{NotebookToLaTeX.py → notebook_to_latex.py} +0 -0
- /edsl/{exceptions/questions.py → questions/exceptions.py} +0 -0
- /edsl/questions/{SimpleAskMixin.py → simple_ask_mixin.py} +0 -0
- /edsl/surveys/{Memory.py → memory/memory.py} +0 -0
- /edsl/surveys/{MemoryManagement.py → memory/memory_management.py} +0 -0
- /edsl/surveys/{SurveyCSS.py → survey_css.py} +0 -0
- /edsl/{jobs/tokens/TokenUsage.py → tokens/token_usage.py} +0 -0
- /edsl/{results/MarkdownToDocx.py → utilities/markdown_to_docx.py} +0 -0
- /edsl/{TemplateLoader.py → utilities/template_loader.py} +0 -0
- {edsl-0.1.46.dist-info → edsl-0.1.48.dist-info}/LICENSE +0 -0
- {edsl-0.1.46.dist-info → edsl-0.1.48.dist-info}/WHEEL +0 -0
@@ -20,25 +20,23 @@ with a low (-1) priority.
|
|
20
20
|
import ast
|
21
21
|
import random
|
22
22
|
from typing import Any, Union, List
|
23
|
+
from collections import defaultdict
|
23
24
|
|
24
25
|
|
25
26
|
# from rich import print
|
26
27
|
from simpleeval import EvalWithCompoundTypes
|
27
28
|
|
28
|
-
from
|
29
|
+
from ..exceptions import SurveyError
|
29
30
|
|
30
|
-
from
|
31
|
+
from ..exceptions import (
|
31
32
|
SurveyRuleCannotEvaluateError,
|
32
|
-
SurveyRuleCollectionHasNoRulesAtNodeError,
|
33
33
|
SurveyRuleRefersToFutureStateError,
|
34
|
-
SurveyRuleReferenceInRuleToUnknownQuestionError,
|
35
34
|
SurveyRuleSendsYouBackwardsError,
|
36
35
|
SurveyRuleSkipLogicSyntaxError,
|
37
36
|
)
|
38
|
-
from edsl.surveys.base import EndOfSurvey
|
39
|
-
from edsl.utilities.ast_utilities import extract_variable_names
|
40
|
-
from edsl.utilities.remove_edsl_version import remove_edsl_version
|
41
37
|
|
38
|
+
from ..base import EndOfSurvey
|
39
|
+
from ...utilities import extract_variable_names, remove_edsl_version
|
42
40
|
|
43
41
|
class QuestionIndex:
|
44
42
|
def __set_name__(self, owner, name):
|
@@ -58,19 +56,11 @@ class QuestionIndex:
|
|
58
56
|
|
59
57
|
|
60
58
|
class Rule:
|
61
|
-
"""The Rule class defines a "rule" for determining the next question
|
59
|
+
"""The Rule class defines a "rule" for determining the next question present."""
|
62
60
|
|
63
61
|
current_q = QuestionIndex()
|
64
62
|
next_q = QuestionIndex()
|
65
63
|
|
66
|
-
# Not implemented but nice to have:
|
67
|
-
# We could potentially use the question pydantic models to check for rule conflicts, as
|
68
|
-
# they define the potential trees through a survey.
|
69
|
-
|
70
|
-
# We could also use the AST to check for conflicts by inspecting the types of a rule.
|
71
|
-
# For example, if we know the answer to a question is a string, we could check that
|
72
|
-
# the expression only contains string comparisons.
|
73
|
-
|
74
64
|
def __init__(
|
75
65
|
self,
|
76
66
|
current_q: int,
|
@@ -114,8 +104,6 @@ class Rule:
|
|
114
104
|
f"The expression {self.expression} is not valid Python syntax."
|
115
105
|
)
|
116
106
|
|
117
|
-
# get the names of the variables in the expression
|
118
|
-
# e.g., q1 == 'yes' -> ['q1']
|
119
107
|
extracted_question_names = extract_variable_names(self.ast_tree)
|
120
108
|
|
121
109
|
# make sure all the variables in the expression are known questions
|
@@ -123,11 +111,6 @@ class Rule:
|
|
123
111
|
assert all([q in question_name_to_index for q in extracted_question_names])
|
124
112
|
except AssertionError:
|
125
113
|
pass
|
126
|
-
# import warnings
|
127
|
-
# warnings.warn(f"There is an extracted field in the expression that is not a known question. It could be a scenario variable. That's fine! But it also could be a typo or mistake.")
|
128
|
-
# print(f"Question name to index: {question_name_to_index}")
|
129
|
-
# print(f"Extracted question names: {extracted_question_names}")
|
130
|
-
# raise SurveyRuleReferenceInRuleToUnknownQuestionError
|
131
114
|
|
132
115
|
# get the indices of the questions mentioned in the expression
|
133
116
|
self.named_questions_by_index = [
|
@@ -144,6 +127,16 @@ class Rule:
|
|
144
127
|
"A rule refers to a future question, the answer to which would not be available here."
|
145
128
|
)
|
146
129
|
raise SurveyRuleRefersToFutureStateError
|
130
|
+
|
131
|
+
if (referenced_questions := self._prior_question_is_in_expression()) and not self._is_jinja2_expression(): #raise ValueError("This uses the old syntax!")
|
132
|
+
import warnings
|
133
|
+
old_expression = self.expression
|
134
|
+
for q in referenced_questions:
|
135
|
+
if q + ".answer" in self.expression:
|
136
|
+
self.expression = self.expression.replace(q + ".answer", f"{{{{ {q}.answer }}}}")
|
137
|
+
else:
|
138
|
+
self.expression = self.expression.replace(q, f"{{{{ {q}.answer }}}}")
|
139
|
+
warnings.warn(f"This uses the old syntax! Converting to Jinja2 style with {{ }}.\nOld expression: {old_expression}\nNew expression: {self.expression}")
|
147
140
|
|
148
141
|
def _checks(self):
|
149
142
|
pass
|
@@ -153,7 +146,7 @@ class Rule:
|
|
153
146
|
|
154
147
|
>>> r = Rule.example()
|
155
148
|
>>> r.to_dict()
|
156
|
-
{'current_q': 1, 'expression': "q1 == 'yes'", 'next_q': 2, 'priority': 0, 'question_name_to_index': {'q1': 1}, 'before_rule': False}
|
149
|
+
{'current_q': 1, 'expression': "{{ q1.answer }} == 'yes'", 'next_q': 2, 'priority': 0, 'question_name_to_index': {'q1': 1}, 'before_rule': False}
|
157
150
|
"""
|
158
151
|
return {
|
159
152
|
"current_q": self.current_q,
|
@@ -225,6 +218,14 @@ class Rule:
|
|
225
218
|
replacement = str(value)
|
226
219
|
d[var] = replacement
|
227
220
|
return d
|
221
|
+
|
222
|
+
def _prior_question_is_in_expression(self) -> set:
|
223
|
+
"""Check if the expression contains a reference to a prior question."""
|
224
|
+
return {q for q in self.question_name_to_index.keys() if q in self.expression}
|
225
|
+
|
226
|
+
def _is_jinja2_expression(self):
|
227
|
+
"""Check if the expression is a Jinja2 expression."""
|
228
|
+
return "{{" in self.expression and "}}" in self.expression
|
228
229
|
|
229
230
|
def evaluate(self, current_info_env: dict[int, Any]):
|
230
231
|
"""Compute the value of the expression, given a dictionary of known questions answers.
|
@@ -234,27 +235,89 @@ class Rule:
|
|
234
235
|
If the expression cannot be evaluated, it raises a CannotEvaluate exception.
|
235
236
|
|
236
237
|
>>> r = Rule.example()
|
237
|
-
>>> r.evaluate({'q1' : 'yes'})
|
238
|
+
>>> r.evaluate({'q1.answer' : 'yes'})
|
238
239
|
True
|
239
|
-
>>> r.evaluate({'q1' : 'no'})
|
240
|
+
>>> r.evaluate({'q1.answer' : 'no'})
|
240
241
|
False
|
241
242
|
|
242
243
|
>>> r = Rule.example(jinja2=True)
|
243
|
-
>>> r.evaluate({'q1' : 'yes'})
|
244
|
+
>>> r.evaluate({'q1.answer' : 'yes'})
|
244
245
|
True
|
245
246
|
|
246
247
|
>>> r = Rule.example(jinja2=True)
|
247
|
-
>>> r.evaluate({'q1' : 'This is q1'})
|
248
|
+
>>> r.evaluate({'q1.answer' : 'This is q1'})
|
248
249
|
False
|
249
250
|
|
250
|
-
>>>
|
251
|
-
>>>
|
252
|
-
|
253
|
-
...
|
254
|
-
|
251
|
+
>>> import warnings
|
252
|
+
>>> with warnings.catch_warnings(record=True) as w:
|
253
|
+
... expression = "q1 == 'yes'"
|
254
|
+
... r = Rule(current_q=1, expression=expression, next_q=2, question_name_to_index={"q1": 1}, priority=0)
|
255
|
+
... result = r.evaluate({'q1.answer' : 'yes'})
|
256
|
+
... assert len(w) == 1 # Verify warning was issued
|
257
|
+
... assert result == True
|
255
258
|
"""
|
256
259
|
from jinja2 import Template
|
257
260
|
|
261
|
+
def jinja_ize_dictionary(dictionary):
|
262
|
+
"""Convert a dictionary to a Jinja2 dictionary.
|
263
|
+
|
264
|
+
Keys must be either:
|
265
|
+
- 'agent'
|
266
|
+
- 'scenario'
|
267
|
+
- A valid question name from question_name_to_index
|
268
|
+
|
269
|
+
For question keys, the value is wrapped in an 'answer' subdictionary.
|
270
|
+
|
271
|
+
Examples:
|
272
|
+
>>> d = jinja_ize_dictionary({'q1': 'yes'}, {'q1': 1})
|
273
|
+
>>> d['q1']['answer']
|
274
|
+
'yes'
|
275
|
+
|
276
|
+
>>> d = jinja_ize_dictionary({'agent': 'friendly'}, {'q1': 1})
|
277
|
+
>>> d['agent']
|
278
|
+
'friendly'
|
279
|
+
"""
|
280
|
+
jinja_dict = defaultdict(dict)
|
281
|
+
|
282
|
+
for key, value in dictionary.items():
|
283
|
+
# print("Now processing key: ", key)
|
284
|
+
# print(f"key: {key}, value: {value}")
|
285
|
+
# Handle special keys
|
286
|
+
if 'agent.' in key:
|
287
|
+
# print("Agent key found")
|
288
|
+
jinja_dict['agent'][key.split('.')[1]] = value
|
289
|
+
# print("jinja dict: ", jinja_dict)
|
290
|
+
continue
|
291
|
+
|
292
|
+
if 'scenario.' in key:
|
293
|
+
# print("Scenario key found")
|
294
|
+
jinja_dict['scenario'][key.split('.')[1]] = value
|
295
|
+
# print("jinja dict: ", jinja_dict)
|
296
|
+
continue
|
297
|
+
|
298
|
+
# print("On to question keys")
|
299
|
+
for question_name in self.question_name_to_index.keys():
|
300
|
+
# print("question_name: ", question_name)
|
301
|
+
if question_name in key:
|
302
|
+
if question_name == key:
|
303
|
+
# print("question name is key; it's an answer")
|
304
|
+
jinja_dict[question_name]['answer'] = value
|
305
|
+
# print("jinja dict: ", jinja_dict)
|
306
|
+
continue
|
307
|
+
else:
|
308
|
+
# print("question name is not key; it's a sub-type")
|
309
|
+
if "." in key:
|
310
|
+
passed_name, value_type = key.split('.')
|
311
|
+
# print("passed_name: ", passed_name)
|
312
|
+
# print("value_type: ", value_type)
|
313
|
+
if passed_name == question_name:
|
314
|
+
# print("passed name is question name; it's a sub-type")
|
315
|
+
jinja_dict[question_name][value_type] = value
|
316
|
+
# print("jinja dict: ", jinja_dict)
|
317
|
+
continue
|
318
|
+
|
319
|
+
return jinja_dict
|
320
|
+
|
258
321
|
def substitute_in_answers(expression, current_info_env):
|
259
322
|
"""Take the dictionary of answers and substitute them into the expression."""
|
260
323
|
|
@@ -262,19 +325,16 @@ class Rule:
|
|
262
325
|
|
263
326
|
if "{{" in expression and "}}" in expression:
|
264
327
|
template_expression = Template(self.expression)
|
265
|
-
|
328
|
+
jinja_dict = jinja_ize_dictionary(current_info)
|
329
|
+
to_evaluate = template_expression.render(jinja_dict)
|
266
330
|
else:
|
267
|
-
# import warnings
|
268
|
-
# import textwrap
|
269
|
-
# warnings.warn(textwrap.dedent("""\
|
270
|
-
# The expression is not a Jinja2 template with {{ }}. This is not recommended.
|
271
|
-
# You can re-write your expression say "q1 == 'yes'" as "{{ q1 }} == 'yes'".
|
272
|
-
# """))
|
273
331
|
to_evaluate = expression
|
274
332
|
for var, value in current_info.items():
|
275
333
|
to_evaluate = to_evaluate.replace(var, value)
|
276
334
|
|
277
335
|
return to_evaluate
|
336
|
+
|
337
|
+
#breakpoint()
|
278
338
|
|
279
339
|
try:
|
280
340
|
to_evaluate = substitute_in_answers(self.expression, current_info_env)
|
@@ -300,16 +360,16 @@ class Rule:
|
|
300
360
|
def example(cls, jinja2=False, bad=False):
|
301
361
|
if jinja2:
|
302
362
|
# a rule written in jinja2 style with {{ }}
|
303
|
-
expression = "{{ q1 }} == 'yes'"
|
363
|
+
expression = "{{ q1.answer }} == 'yes'"
|
304
364
|
else:
|
305
|
-
expression = "q1 == 'yes'"
|
365
|
+
expression = "{{ q1.answer }} == 'yes'"
|
306
366
|
|
307
367
|
if bad and jinja2:
|
308
368
|
# a rule written in jinja2 style with {{ }} but with a 'bad' expression
|
309
369
|
expression = "{{ q1 }} == 'This is q1'"
|
310
370
|
|
311
371
|
if bad and not jinja2:
|
312
|
-
expression = "q1 == 'This is q1'"
|
372
|
+
expression = "{{ q1.answer }} == 'This is q1'"
|
313
373
|
|
314
374
|
r = Rule(
|
315
375
|
current_q=1,
|
@@ -3,15 +3,14 @@
|
|
3
3
|
from typing import List, Union, Any, Optional
|
4
4
|
from collections import defaultdict, UserList, namedtuple
|
5
5
|
|
6
|
-
from
|
6
|
+
from ..exceptions import (
|
7
7
|
SurveyRuleCannotEvaluateError,
|
8
8
|
SurveyRuleCollectionHasNoRulesAtNodeError,
|
9
9
|
)
|
10
10
|
|
11
|
-
from
|
12
|
-
from
|
13
|
-
from
|
14
|
-
|
11
|
+
from .rule import Rule
|
12
|
+
from ..base import EndOfSurvey
|
13
|
+
from ..dag import DAG
|
15
14
|
|
16
15
|
NextQuestion = namedtuple(
|
17
16
|
"NextQuestion", "next_q, num_rules_found, expressions_evaluating_to_true, priority"
|
@@ -45,7 +44,7 @@ class RuleCollection(UserList):
|
|
45
44
|
|
46
45
|
def to_dataset(self):
|
47
46
|
"""Return a Dataset object representation of the RuleCollection object."""
|
48
|
-
from
|
47
|
+
from ...dataset import Dataset
|
49
48
|
|
50
49
|
keys = ["current_q", "expression", "next_q", "priority", "before_rule"]
|
51
50
|
rule_list = {}
|
@@ -56,13 +55,21 @@ class RuleCollection(UserList):
|
|
56
55
|
return Dataset([{k: v} for k, v in rule_list.items()])
|
57
56
|
|
58
57
|
def _repr_html_(self):
|
59
|
-
"""Return an HTML representation of the RuleCollection object.
|
60
|
-
|
61
|
-
|
58
|
+
"""Return an HTML representation of the RuleCollection object.
|
59
|
+
|
60
|
+
>>> rule_collection = RuleCollection.example()
|
61
|
+
>>> _ = rule_collection._repr_html_()
|
62
|
+
"""
|
62
63
|
return self.to_dataset()._repr_html_()
|
63
64
|
|
64
65
|
def to_dict(self, add_edsl_version=True):
|
65
|
-
"""Create a dictionary representation of the RuleCollection object.
|
66
|
+
"""Create a dictionary representation of the RuleCollection object.
|
67
|
+
>>> rule_collection = RuleCollection.example()
|
68
|
+
>>> rule_collection_dict = rule_collection.to_dict()
|
69
|
+
>>> new_rule_collection = RuleCollection.from_dict(rule_collection_dict)
|
70
|
+
>>> repr(new_rule_collection) == repr(rule_collection)
|
71
|
+
True
|
72
|
+
"""
|
66
73
|
return {
|
67
74
|
"rules": [rule.to_dict() for rule in self],
|
68
75
|
"num_questions": self.num_questions,
|
@@ -108,18 +115,6 @@ class RuleCollection(UserList):
|
|
108
115
|
|
109
116
|
def show_rules(self) -> None:
|
110
117
|
"""Print the rules in a table.
|
111
|
-
|
112
|
-
|
113
|
-
.. code-block:: python
|
114
|
-
|
115
|
-
rule_collection = RuleCollection.example()
|
116
|
-
rule_collection.show_rules()
|
117
|
-
┏━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━┓
|
118
|
-
┃ current_q ┃ expression ┃ next_q ┃ priority ┃ before_rule ┃
|
119
|
-
┡━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━┩
|
120
|
-
│ 1 │ q1 == 'yes' │ 3 │ 1 │ False │
|
121
|
-
│ 1 │ q1 == 'no' │ 2 │ 1 │ False │
|
122
|
-
└───────────┴─────────────┴────────┴──────────┴─────────────┘
|
123
118
|
"""
|
124
119
|
return self.to_dataset()
|
125
120
|
|
@@ -140,7 +135,6 @@ class RuleCollection(UserList):
|
|
140
135
|
>>> rule_collection.add_rule(r)
|
141
136
|
>>> rule_collection.skip_question_before_running(1, {})
|
142
137
|
False
|
143
|
-
|
144
138
|
"""
|
145
139
|
for rule in self.applicable_rules(q_now, before_rule=True):
|
146
140
|
if rule.evaluate(answers):
|
@@ -157,7 +151,7 @@ class RuleCollection(UserList):
|
|
157
151
|
|
158
152
|
>>> rule_collection = RuleCollection.example()
|
159
153
|
>>> rule_collection.applicable_rules(1)
|
160
|
-
[Rule(current_q=1, expression="q1 == 'yes'", next_q=3, priority=1, question_name_to_index={'q1': 1, 'q2': 2, 'q3': 3, 'q4': 4}, before_rule=False), Rule(current_q=1, expression="q1 == 'no'", next_q=2, priority=1, question_name_to_index={'q1': 1, 'q2': 2, 'q3': 3, 'q4': 4}, before_rule=False)]
|
154
|
+
[Rule(current_q=1, expression="{{ q1.answer }} == 'yes'", next_q=3, priority=1, question_name_to_index={'q1': 1, 'q2': 2, 'q3': 3, 'q4': 4}, before_rule=False), Rule(current_q=1, expression="{{ q1.answer }} == 'no'", next_q=2, priority=1, question_name_to_index={'q1': 1, 'q2': 2, 'q3': 3, 'q4': 4}, before_rule=False)]
|
161
155
|
|
162
156
|
The default is that the rule is applied after the question is asked.
|
163
157
|
If we want to see the rules that apply before the question is asked, we can set before_rule=True.
|
@@ -189,7 +183,7 @@ class RuleCollection(UserList):
|
|
189
183
|
:param answers: The answers to the survey questions so far, including the current question.
|
190
184
|
|
191
185
|
>>> rule_collection = RuleCollection.example()
|
192
|
-
>>> rule_collection.next_question(1, {'q1': 'yes'})
|
186
|
+
>>> rule_collection.next_question(1, {'q1.answer': 'yes'})
|
193
187
|
NextQuestion(next_q=3, num_rules_found=2, expressions_evaluating_to_true=1, priority=1)
|
194
188
|
|
195
189
|
"""
|
@@ -361,14 +355,14 @@ class RuleCollection(UserList):
|
|
361
355
|
rules=[
|
362
356
|
Rule(
|
363
357
|
current_q=1,
|
364
|
-
expression="q1 == 'yes'",
|
358
|
+
expression="{{ q1.answer }} == 'yes'",
|
365
359
|
next_q=3,
|
366
360
|
priority=1,
|
367
361
|
question_name_to_index=qn2i,
|
368
362
|
),
|
369
363
|
Rule(
|
370
364
|
current_q=1,
|
371
|
-
expression="q1 == 'no'",
|
365
|
+
expression="{{ q1.answer }} == 'no'",
|
372
366
|
next_q=2,
|
373
367
|
priority=1,
|
374
368
|
question_name_to_index=qn2i,
|
@@ -379,7 +373,4 @@ class RuleCollection(UserList):
|
|
379
373
|
|
380
374
|
if __name__ == "__main__":
|
381
375
|
import doctest
|
382
|
-
|
383
376
|
doctest.testmod(optionflags=doctest.ELLIPSIS)
|
384
|
-
|
385
|
-
print(RuleCollection.example()._repr_html_())
|
@@ -1,12 +1,12 @@
|
|
1
1
|
from typing import Union, TYPE_CHECKING
|
2
2
|
|
3
3
|
if TYPE_CHECKING:
|
4
|
-
from
|
5
|
-
|
6
|
-
from edsl.surveys.Rule import Rule
|
7
|
-
from .base import RulePriority, EndOfSurvey
|
8
|
-
from edsl.exceptions.surveys import SurveyError, SurveyCreationError
|
4
|
+
from ...questions import QuestionBase
|
5
|
+
from ..survey import Survey
|
9
6
|
|
7
|
+
from ..exceptions import SurveyError, SurveyCreationError
|
8
|
+
from .rule import Rule
|
9
|
+
from ..base import RulePriority, EndOfSurvey
|
10
10
|
|
11
11
|
class ValidatedString(str):
|
12
12
|
def __new__(cls, content):
|
@@ -22,8 +22,8 @@ class RuleManager:
|
|
22
22
|
self.survey = survey
|
23
23
|
|
24
24
|
def _get_question_index(
|
25
|
-
self, q: Union["QuestionBase", str, EndOfSurvey
|
26
|
-
) -> Union[int, EndOfSurvey
|
25
|
+
self, q: Union["QuestionBase", str, 'EndOfSurvey']
|
26
|
+
) -> Union[int, 'EndOfSurvey']:
|
27
27
|
"""Return the index of the question or EndOfSurvey object.
|
28
28
|
|
29
29
|
:param q: The question or question name to get the index of.
|
@@ -41,7 +41,7 @@ class RuleManager:
|
|
41
41
|
>>> s._get_question_index("poop")
|
42
42
|
Traceback (most recent call last):
|
43
43
|
...
|
44
|
-
edsl.exceptions.
|
44
|
+
edsl.surveys.exceptions.SurveyError: Question name poop not found in survey. The current question names are {'q0': 0, 'q1': 1, 'q2': 2}.
|
45
45
|
...
|
46
46
|
"""
|
47
47
|
if q == EndOfSurvey:
|
@@ -141,19 +141,19 @@ class RuleManager:
|
|
141
141
|
Here, answering "yes" to q0 ends the survey:
|
142
142
|
|
143
143
|
>>> from edsl import Survey
|
144
|
-
>>> s = Survey.example().add_stop_rule("q0", "q0 == 'yes'")
|
145
|
-
>>> s.next_question("q0", {"q0": "yes"})
|
144
|
+
>>> s = Survey.example().add_stop_rule("q0", "{{ q0.answer }} == 'yes'")
|
145
|
+
>>> s.next_question("q0", {"q0.answer": "yes"})
|
146
146
|
EndOfSurvey
|
147
147
|
|
148
148
|
By comparison, answering "no" to q0 does not end the survey:
|
149
149
|
|
150
|
-
>>> s.next_question("q0", {"q0": "no"}).question_name
|
150
|
+
>>> s.next_question("q0", {"q0.answer": "no"}).question_name
|
151
151
|
'q1'
|
152
152
|
|
153
|
-
>>> s.add_stop_rule("q0", "q1 <> 'yes'")
|
153
|
+
>>> s.add_stop_rule("q0", "{{ q1.answer }} <> 'yes'")
|
154
154
|
Traceback (most recent call last):
|
155
155
|
...
|
156
|
-
edsl.exceptions.
|
156
|
+
edsl.surveys.exceptions.SurveyCreationError: The expression contains '<>', which is not allowed. You probably mean '!='.
|
157
157
|
...
|
158
158
|
"""
|
159
159
|
expression = ValidatedString(expression)
|
@@ -170,3 +170,9 @@ class RuleManager:
|
|
170
170
|
)
|
171
171
|
self.survey.add_rule(question, expression, EndOfSurvey)
|
172
172
|
return self.survey
|
173
|
+
|
174
|
+
|
175
|
+
if __name__ == "__main__":
|
176
|
+
import doctest
|
177
|
+
|
178
|
+
doctest.testmod(optionflags=doctest.ELLIPSIS)
|