edsl 0.1.49__py3-none-any.whl → 0.1.51__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 +124 -53
- edsl/__version__.py +1 -1
- edsl/agents/agent.py +21 -21
- edsl/agents/agent_list.py +2 -5
- edsl/agents/exceptions.py +119 -5
- edsl/base/__init__.py +10 -35
- edsl/base/base_class.py +71 -36
- edsl/base/base_exception.py +204 -0
- edsl/base/data_transfer_models.py +1 -1
- edsl/base/exceptions.py +94 -0
- edsl/buckets/__init__.py +15 -1
- edsl/buckets/bucket_collection.py +3 -4
- edsl/buckets/exceptions.py +107 -0
- edsl/buckets/model_buckets.py +1 -2
- edsl/buckets/token_bucket.py +11 -6
- edsl/buckets/token_bucket_api.py +27 -12
- edsl/buckets/token_bucket_client.py +9 -7
- edsl/caching/cache.py +12 -4
- edsl/caching/cache_entry.py +10 -9
- edsl/caching/exceptions.py +113 -7
- edsl/caching/remote_cache_sync.py +6 -7
- edsl/caching/sql_dict.py +20 -14
- edsl/cli.py +43 -0
- edsl/config/__init__.py +1 -1
- edsl/config/config_class.py +32 -6
- edsl/conversation/Conversation.py +8 -4
- edsl/conversation/car_buying.py +1 -3
- edsl/conversation/exceptions.py +58 -0
- edsl/conversation/mug_negotiation.py +2 -8
- edsl/coop/__init__.py +28 -6
- edsl/coop/coop.py +120 -29
- edsl/coop/coop_functions.py +1 -1
- edsl/coop/ep_key_handling.py +1 -1
- edsl/coop/exceptions.py +188 -9
- edsl/coop/price_fetcher.py +5 -8
- edsl/coop/utils.py +4 -6
- edsl/dataset/__init__.py +5 -4
- edsl/dataset/dataset.py +177 -86
- edsl/dataset/dataset_operations_mixin.py +98 -76
- edsl/dataset/dataset_tree.py +11 -7
- edsl/dataset/display/table_display.py +0 -2
- edsl/dataset/display/table_renderers.py +6 -4
- edsl/dataset/exceptions.py +125 -0
- edsl/dataset/file_exports.py +18 -11
- edsl/dataset/r/ggplot.py +13 -6
- edsl/display/__init__.py +27 -0
- edsl/display/core.py +147 -0
- edsl/display/plugin.py +189 -0
- edsl/display/utils.py +52 -0
- edsl/inference_services/__init__.py +9 -1
- edsl/inference_services/available_model_cache_handler.py +1 -1
- edsl/inference_services/available_model_fetcher.py +5 -6
- edsl/inference_services/data_structures.py +10 -7
- edsl/inference_services/exceptions.py +132 -1
- edsl/inference_services/inference_service_abc.py +2 -2
- edsl/inference_services/inference_services_collection.py +2 -6
- edsl/inference_services/registry.py +4 -3
- edsl/inference_services/service_availability.py +4 -3
- edsl/inference_services/services/anthropic_service.py +4 -1
- edsl/inference_services/services/aws_bedrock.py +13 -12
- edsl/inference_services/services/azure_ai.py +12 -10
- edsl/inference_services/services/deep_infra_service.py +1 -4
- edsl/inference_services/services/deep_seek_service.py +1 -5
- edsl/inference_services/services/google_service.py +7 -3
- edsl/inference_services/services/groq_service.py +1 -1
- edsl/inference_services/services/mistral_ai_service.py +4 -2
- edsl/inference_services/services/ollama_service.py +1 -1
- edsl/inference_services/services/open_ai_service.py +7 -5
- edsl/inference_services/services/perplexity_service.py +6 -2
- edsl/inference_services/services/test_service.py +8 -7
- edsl/inference_services/services/together_ai_service.py +2 -3
- edsl/inference_services/services/xai_service.py +1 -1
- edsl/instructions/__init__.py +1 -1
- edsl/instructions/change_instruction.py +7 -5
- edsl/instructions/exceptions.py +61 -0
- edsl/instructions/instruction.py +6 -2
- edsl/instructions/instruction_collection.py +6 -4
- edsl/instructions/instruction_handler.py +12 -15
- edsl/interviews/ReportErrors.py +0 -3
- edsl/interviews/__init__.py +9 -2
- edsl/interviews/answering_function.py +11 -13
- edsl/interviews/exception_tracking.py +15 -8
- edsl/interviews/exceptions.py +79 -0
- edsl/interviews/interview.py +33 -30
- edsl/interviews/interview_status_dictionary.py +4 -2
- edsl/interviews/interview_status_log.py +2 -1
- edsl/interviews/interview_task_manager.py +5 -5
- edsl/interviews/request_token_estimator.py +5 -2
- edsl/interviews/statistics.py +3 -4
- edsl/invigilators/__init__.py +7 -1
- edsl/invigilators/exceptions.py +79 -0
- edsl/invigilators/invigilator_base.py +0 -1
- edsl/invigilators/invigilators.py +9 -13
- edsl/invigilators/prompt_constructor.py +1 -5
- edsl/invigilators/prompt_helpers.py +8 -4
- edsl/invigilators/question_instructions_prompt_builder.py +1 -1
- edsl/invigilators/question_option_processor.py +9 -5
- edsl/invigilators/question_template_replacements_builder.py +3 -2
- edsl/jobs/__init__.py +42 -5
- edsl/jobs/async_interview_runner.py +25 -23
- edsl/jobs/check_survey_scenario_compatibility.py +11 -10
- edsl/jobs/data_structures.py +8 -5
- edsl/jobs/exceptions.py +177 -8
- edsl/jobs/fetch_invigilator.py +1 -1
- edsl/jobs/jobs.py +74 -69
- edsl/jobs/jobs_checks.py +6 -7
- edsl/jobs/jobs_component_constructor.py +4 -4
- edsl/jobs/jobs_pricing_estimation.py +4 -3
- edsl/jobs/jobs_remote_inference_logger.py +5 -4
- edsl/jobs/jobs_runner_asyncio.py +3 -4
- edsl/jobs/jobs_runner_status.py +8 -9
- edsl/jobs/remote_inference.py +27 -24
- edsl/jobs/results_exceptions_handler.py +10 -7
- edsl/key_management/__init__.py +3 -1
- edsl/key_management/exceptions.py +62 -0
- edsl/key_management/key_lookup.py +1 -1
- edsl/key_management/key_lookup_builder.py +37 -14
- edsl/key_management/key_lookup_collection.py +2 -0
- edsl/language_models/__init__.py +1 -1
- edsl/language_models/exceptions.py +302 -14
- edsl/language_models/language_model.py +9 -8
- edsl/language_models/model.py +4 -4
- edsl/language_models/model_list.py +1 -1
- edsl/language_models/price_manager.py +1 -1
- edsl/language_models/raw_response_handler.py +14 -9
- edsl/language_models/registry.py +17 -21
- edsl/language_models/repair.py +0 -6
- edsl/language_models/unused/fake_openai_service.py +0 -1
- edsl/load_plugins.py +69 -0
- edsl/logger.py +146 -0
- edsl/notebooks/__init__.py +24 -1
- edsl/notebooks/exceptions.py +82 -0
- edsl/notebooks/notebook.py +7 -3
- edsl/notebooks/notebook_to_latex.py +1 -2
- edsl/plugins/__init__.py +63 -0
- edsl/plugins/built_in/export_example.py +50 -0
- edsl/plugins/built_in/pig_latin.py +67 -0
- edsl/plugins/cli.py +372 -0
- edsl/plugins/cli_typer.py +283 -0
- edsl/plugins/exceptions.py +31 -0
- edsl/plugins/hookspec.py +51 -0
- edsl/plugins/plugin_host.py +128 -0
- edsl/plugins/plugin_manager.py +633 -0
- edsl/plugins/plugins_registry.py +168 -0
- edsl/prompts/__init__.py +24 -1
- edsl/prompts/exceptions.py +107 -5
- edsl/prompts/prompt.py +15 -7
- edsl/questions/HTMLQuestion.py +5 -11
- edsl/questions/Quick.py +0 -1
- edsl/questions/__init__.py +6 -4
- edsl/questions/answer_validator_mixin.py +318 -323
- edsl/questions/compose_questions.py +3 -3
- edsl/questions/descriptors.py +11 -50
- edsl/questions/exceptions.py +278 -22
- edsl/questions/loop_processor.py +7 -5
- edsl/questions/prompt_templates/question_list.jinja +3 -0
- edsl/questions/question_base.py +46 -19
- edsl/questions/question_base_gen_mixin.py +2 -2
- edsl/questions/question_base_prompts_mixin.py +13 -7
- edsl/questions/question_budget.py +503 -98
- edsl/questions/question_check_box.py +660 -160
- edsl/questions/question_dict.py +345 -194
- edsl/questions/question_extract.py +401 -61
- edsl/questions/question_free_text.py +80 -14
- edsl/questions/question_functional.py +119 -9
- edsl/questions/{derived/question_likert_five.py → question_likert_five.py} +2 -2
- edsl/questions/{derived/question_linear_scale.py → question_linear_scale.py} +3 -4
- edsl/questions/question_list.py +275 -28
- edsl/questions/question_matrix.py +643 -96
- edsl/questions/question_multiple_choice.py +219 -51
- edsl/questions/question_numerical.py +361 -32
- edsl/questions/question_rank.py +401 -124
- edsl/questions/question_registry.py +7 -5
- edsl/questions/{derived/question_top_k.py → question_top_k.py} +3 -3
- edsl/questions/{derived/question_yes_no.py → question_yes_no.py} +3 -4
- edsl/questions/register_questions_meta.py +2 -2
- edsl/questions/response_validator_abc.py +13 -15
- edsl/questions/response_validator_factory.py +10 -12
- edsl/questions/templates/dict/answering_instructions.jinja +1 -0
- edsl/questions/templates/rank/question_presentation.jinja +1 -1
- edsl/results/__init__.py +1 -1
- edsl/results/exceptions.py +141 -7
- edsl/results/report.py +1 -2
- edsl/results/result.py +11 -9
- edsl/results/results.py +480 -321
- edsl/results/results_selector.py +8 -4
- edsl/scenarios/PdfExtractor.py +2 -2
- edsl/scenarios/construct_download_link.py +69 -35
- edsl/scenarios/directory_scanner.py +33 -14
- edsl/scenarios/document_chunker.py +1 -1
- edsl/scenarios/exceptions.py +238 -14
- edsl/scenarios/file_methods.py +1 -1
- edsl/scenarios/file_store.py +7 -3
- edsl/scenarios/handlers/__init__.py +17 -0
- edsl/scenarios/handlers/docx_file_store.py +0 -5
- edsl/scenarios/handlers/pdf_file_store.py +0 -1
- edsl/scenarios/handlers/pptx_file_store.py +0 -5
- edsl/scenarios/handlers/py_file_store.py +0 -1
- edsl/scenarios/handlers/sql_file_store.py +1 -4
- edsl/scenarios/handlers/sqlite_file_store.py +0 -1
- edsl/scenarios/handlers/txt_file_store.py +1 -1
- edsl/scenarios/scenario.py +1 -3
- edsl/scenarios/scenario_list.py +179 -27
- edsl/scenarios/scenario_list_pdf_tools.py +1 -0
- edsl/scenarios/scenario_selector.py +0 -1
- edsl/surveys/__init__.py +3 -4
- edsl/surveys/dag/__init__.py +4 -2
- edsl/surveys/descriptors.py +1 -1
- edsl/surveys/edit_survey.py +1 -0
- edsl/surveys/exceptions.py +165 -9
- edsl/surveys/memory/__init__.py +5 -3
- edsl/surveys/memory/memory_management.py +1 -0
- edsl/surveys/memory/memory_plan.py +6 -15
- edsl/surveys/rules/__init__.py +5 -3
- edsl/surveys/rules/rule.py +1 -2
- edsl/surveys/rules/rule_collection.py +1 -1
- edsl/surveys/survey.py +12 -24
- edsl/surveys/survey_css.py +3 -3
- edsl/surveys/survey_export.py +6 -3
- edsl/surveys/survey_flow_visualization.py +10 -1
- edsl/surveys/survey_simulator.py +2 -1
- edsl/tasks/__init__.py +23 -1
- edsl/tasks/exceptions.py +72 -0
- edsl/tasks/question_task_creator.py +3 -3
- edsl/tasks/task_creators.py +1 -3
- edsl/tasks/task_history.py +8 -10
- edsl/tasks/task_status_log.py +1 -2
- edsl/tokens/__init__.py +29 -1
- edsl/tokens/exceptions.py +37 -0
- edsl/tokens/interview_token_usage.py +3 -2
- edsl/tokens/token_usage.py +4 -3
- edsl/utilities/__init__.py +21 -1
- edsl/utilities/decorators.py +1 -2
- edsl/utilities/markdown_to_docx.py +2 -2
- edsl/utilities/markdown_to_pdf.py +1 -1
- edsl/utilities/repair_functions.py +0 -1
- edsl/utilities/restricted_python.py +0 -1
- edsl/utilities/template_loader.py +2 -3
- edsl/utilities/utilities.py +8 -29
- {edsl-0.1.49.dist-info → edsl-0.1.51.dist-info}/METADATA +32 -2
- edsl-0.1.51.dist-info/RECORD +365 -0
- edsl-0.1.51.dist-info/entry_points.txt +3 -0
- edsl/dataset/smart_objects.py +0 -96
- edsl/exceptions/BaseException.py +0 -21
- edsl/exceptions/__init__.py +0 -54
- edsl/exceptions/configuration.py +0 -16
- edsl/exceptions/general.py +0 -34
- edsl/questions/derived/__init__.py +0 -0
- edsl/study/ObjectEntry.py +0 -173
- edsl/study/ProofOfWork.py +0 -113
- edsl/study/SnapShot.py +0 -80
- edsl/study/Study.py +0 -520
- edsl/study/__init__.py +0 -6
- edsl/utilities/interface.py +0 -135
- edsl-0.1.49.dist-info/RECORD +0 -347
- {edsl-0.1.49.dist-info → edsl-0.1.51.dist-info}/LICENSE +0 -0
- {edsl-0.1.49.dist-info → edsl-0.1.51.dist-info}/WHEEL +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
import tempfile
|
2
2
|
import re
|
3
|
-
from typing import List
|
3
|
+
from typing import List
|
4
4
|
import textwrap
|
5
5
|
|
6
6
|
|
@@ -217,9 +217,6 @@ class SqlMethods(FileMethods):
|
|
217
217
|
This is a simple check and doesn't replace proper SQL parsing.
|
218
218
|
"""
|
219
219
|
try:
|
220
|
-
with open(self.path, "r", encoding="utf-8") as f:
|
221
|
-
content = f.read()
|
222
|
-
|
223
220
|
statements = self.split_statements()
|
224
221
|
for stmt in statements:
|
225
222
|
# Check for basic SQL keywords
|
edsl/scenarios/scenario.py
CHANGED
@@ -20,7 +20,6 @@ information to questions in surveys.
|
|
20
20
|
from __future__ import annotations
|
21
21
|
import copy
|
22
22
|
import os
|
23
|
-
import json
|
24
23
|
from collections import UserDict
|
25
24
|
from typing import Union, List, Optional, TYPE_CHECKING, Collection
|
26
25
|
from uuid import uuid4
|
@@ -97,8 +96,7 @@ class Scenario(Base, UserDict):
|
|
97
96
|
data = dict(data)
|
98
97
|
except Exception as e:
|
99
98
|
raise ScenarioError(
|
100
|
-
f"You must pass in a dictionary to initialize a Scenario. You passed in {data}",
|
101
|
-
"Exception message:" + str(e),
|
99
|
+
f"You must pass in a dictionary to initialize a Scenario. You passed in {data}" + "Exception message:" + str(e),
|
102
100
|
)
|
103
101
|
|
104
102
|
super().__init__()
|
edsl/scenarios/scenario_list.py
CHANGED
@@ -30,6 +30,7 @@ from typing import (
|
|
30
30
|
import warnings
|
31
31
|
import csv
|
32
32
|
import random
|
33
|
+
import os
|
33
34
|
from io import StringIO
|
34
35
|
import inspect
|
35
36
|
from collections import UserList, defaultdict
|
@@ -57,8 +58,9 @@ from ..dataset import ScenarioListOperationsMixin
|
|
57
58
|
|
58
59
|
from .exceptions import ScenarioError
|
59
60
|
from .scenario import Scenario
|
60
|
-
from .directory_scanner import DirectoryScanner
|
61
61
|
from .scenario_list_pdf_tools import PdfTools
|
62
|
+
from .directory_scanner import DirectoryScanner
|
63
|
+
from .file_store import FileStore
|
62
64
|
|
63
65
|
|
64
66
|
if TYPE_CHECKING:
|
@@ -485,7 +487,8 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
|
|
485
487
|
if isinstance(other, Scenario):
|
486
488
|
other = ScenarioList([other])
|
487
489
|
elif not isinstance(other, ScenarioList):
|
488
|
-
|
490
|
+
from .exceptions import TypeScenarioError
|
491
|
+
raise TypeScenarioError(f"Cannot multiply ScenarioList with {type(other)}")
|
489
492
|
|
490
493
|
new_sl = []
|
491
494
|
for s1, s2 in list(product(self, other)):
|
@@ -596,7 +599,8 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
|
|
596
599
|
# Convert to a set (removes duplicates)
|
597
600
|
new_scenario[field_name] = set(values)
|
598
601
|
else:
|
599
|
-
|
602
|
+
from .exceptions import ValueScenarioError
|
603
|
+
raise ValueScenarioError(f"Invalid output_type: {output_type}. Must be 'string', 'list', or 'set'.")
|
600
604
|
|
601
605
|
new_scenarios.append(new_scenario)
|
602
606
|
|
@@ -887,6 +891,127 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
|
|
887
891
|
sl = self.duplicate()
|
888
892
|
return ScenarioList([scenario.keep(fields) for scenario in sl])
|
889
893
|
|
894
|
+
@classmethod
|
895
|
+
def from_directory(
|
896
|
+
cls,
|
897
|
+
path: Optional[str] = None,
|
898
|
+
recursive: bool = False,
|
899
|
+
key_name: str = "content",
|
900
|
+
) -> "ScenarioList":
|
901
|
+
"""Create a ScenarioList of Scenario objects from files in a directory.
|
902
|
+
|
903
|
+
This method scans a directory and creates a Scenario object for each file found,
|
904
|
+
where each Scenario contains a FileStore object under the specified key.
|
905
|
+
Optionally filters files based on a wildcard pattern. If no path is provided,
|
906
|
+
the current working directory is used.
|
907
|
+
|
908
|
+
Args:
|
909
|
+
path: The directory path to scan, optionally including a wildcard pattern.
|
910
|
+
If None, uses the current working directory.
|
911
|
+
Examples:
|
912
|
+
- "/path/to/directory" - scans all files in the directory
|
913
|
+
- "/path/to/directory/*.py" - scans only Python files in the directory
|
914
|
+
- "*.txt" - scans only text files in the current working directory
|
915
|
+
recursive: Whether to scan subdirectories recursively. Defaults to False.
|
916
|
+
key_name: The key to use for the FileStore object in each Scenario. Defaults to "content".
|
917
|
+
|
918
|
+
Returns:
|
919
|
+
A ScenarioList containing Scenario objects for all matching files, where each Scenario
|
920
|
+
has a FileStore object under the specified key.
|
921
|
+
|
922
|
+
Raises:
|
923
|
+
FileNotFoundError: If the specified directory does not exist.
|
924
|
+
|
925
|
+
Examples:
|
926
|
+
# Get all files in the current directory with default key "content"
|
927
|
+
sl = ScenarioList.from_directory()
|
928
|
+
|
929
|
+
# Get all Python files in a specific directory with custom key "python_file"
|
930
|
+
sl = ScenarioList.from_directory('*.py', key_name="python_file")
|
931
|
+
|
932
|
+
# Get all image files in the current directory
|
933
|
+
sl = ScenarioList.from_directory('*.png', key_name="image")
|
934
|
+
|
935
|
+
# Get all files recursively including subdirectories
|
936
|
+
sl = ScenarioList.from_directory(recursive=True, key_name="document")
|
937
|
+
"""
|
938
|
+
# Handle default case - use current directory
|
939
|
+
if path is None:
|
940
|
+
directory_path = os.getcwd()
|
941
|
+
pattern = None
|
942
|
+
else:
|
943
|
+
# Special handling for "**" pattern which indicates recursive scanning
|
944
|
+
has_recursive_pattern = '**' in path if path else False
|
945
|
+
|
946
|
+
# Check if path contains any wildcard
|
947
|
+
if path and ('*' in path):
|
948
|
+
# Handle "**/*.ext" pattern - find the directory part before the **
|
949
|
+
if has_recursive_pattern:
|
950
|
+
# Extract the base directory by finding the part before **
|
951
|
+
parts = path.split('**')
|
952
|
+
if parts and parts[0]:
|
953
|
+
# Remove trailing slash if any
|
954
|
+
directory_path = parts[0].rstrip('/')
|
955
|
+
if not directory_path:
|
956
|
+
directory_path = os.getcwd()
|
957
|
+
# Get the pattern after **
|
958
|
+
pattern = parts[1] if len(parts) > 1 else None
|
959
|
+
if pattern and pattern.startswith('/'):
|
960
|
+
pattern = pattern[1:] # Remove leading slash
|
961
|
+
else:
|
962
|
+
directory_path = os.getcwd()
|
963
|
+
pattern = None
|
964
|
+
# Handle case where path is just a pattern (e.g., "*.py")
|
965
|
+
elif os.path.dirname(path) == '':
|
966
|
+
directory_path = os.getcwd()
|
967
|
+
pattern = os.path.basename(path)
|
968
|
+
else:
|
969
|
+
# Split into directory and pattern
|
970
|
+
directory_path = os.path.dirname(path)
|
971
|
+
if not directory_path:
|
972
|
+
directory_path = os.getcwd()
|
973
|
+
pattern = os.path.basename(path)
|
974
|
+
else:
|
975
|
+
# Path is a directory with no pattern
|
976
|
+
directory_path = path
|
977
|
+
pattern = None
|
978
|
+
|
979
|
+
# Ensure directory exists
|
980
|
+
if not os.path.isdir(directory_path):
|
981
|
+
from .exceptions import FileNotFoundScenarioError
|
982
|
+
raise FileNotFoundScenarioError(f"Directory not found: {directory_path}")
|
983
|
+
|
984
|
+
# Create a DirectoryScanner for the directory
|
985
|
+
scanner = DirectoryScanner(directory_path)
|
986
|
+
|
987
|
+
# Configure wildcard pattern filtering
|
988
|
+
suffix_allow_list = None
|
989
|
+
example_suffix = None
|
990
|
+
|
991
|
+
if pattern:
|
992
|
+
if pattern.startswith('*.'):
|
993
|
+
# Simple extension filter (e.g., "*.py")
|
994
|
+
suffix_allow_list = [pattern[2:]]
|
995
|
+
elif '*' in pattern:
|
996
|
+
# Other wildcard patterns
|
997
|
+
example_suffix = pattern
|
998
|
+
else:
|
999
|
+
# Handle simple non-wildcard pattern (exact match)
|
1000
|
+
example_suffix = pattern
|
1001
|
+
|
1002
|
+
# Use scanner to find files and create FileStore objects
|
1003
|
+
file_stores = scanner.scan(
|
1004
|
+
factory=lambda path: FileStore(path),
|
1005
|
+
recursive=recursive,
|
1006
|
+
suffix_allow_list=suffix_allow_list,
|
1007
|
+
example_suffix=example_suffix
|
1008
|
+
)
|
1009
|
+
|
1010
|
+
# Convert FileStore objects to Scenario objects with the specified key
|
1011
|
+
scenarios = [Scenario({key_name: file_store}) for file_store in file_stores]
|
1012
|
+
|
1013
|
+
return cls(scenarios)
|
1014
|
+
|
890
1015
|
@classmethod
|
891
1016
|
def from_list(
|
892
1017
|
cls, name: str, values: list, func: Optional[Callable] = None
|
@@ -903,7 +1028,9 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
|
|
903
1028
|
ScenarioList([Scenario({'name': 'Alice'}), Scenario({'name': 'Bob'})])
|
904
1029
|
"""
|
905
1030
|
if not func:
|
906
|
-
|
1031
|
+
def identity(x):
|
1032
|
+
return x
|
1033
|
+
func = identity
|
907
1034
|
return cls([Scenario({name: func(value)}) for value in values])
|
908
1035
|
|
909
1036
|
def table(
|
@@ -914,7 +1041,6 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
|
|
914
1041
|
) -> str:
|
915
1042
|
"""Return the ScenarioList as a table."""
|
916
1043
|
|
917
|
-
from tabulate import tabulate_formats
|
918
1044
|
|
919
1045
|
if tablefmt is not None and tablefmt not in tabulate_formats:
|
920
1046
|
raise ValueError(
|
@@ -951,13 +1077,10 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
|
|
951
1077
|
|
952
1078
|
Example:
|
953
1079
|
|
954
|
-
|
955
|
-
|
956
|
-
|
957
|
-
|
958
|
-
Traceback (most recent call last):
|
959
|
-
...
|
960
|
-
AssertionError
|
1080
|
+
# Example:
|
1081
|
+
# s = ScenarioList([Scenario({'a': 1, 'b': 2}), Scenario({'a': 3, 'b': 4})])
|
1082
|
+
# s.reorder_keys(['b', 'a']) # Returns a new ScenarioList with reordered keys
|
1083
|
+
# Attempting s.reorder_keys(['a', 'b', 'c']) would fail as 'c' is not a valid key
|
961
1084
|
"""
|
962
1085
|
assert set(new_order) == set(self.parameters)
|
963
1086
|
|
@@ -1020,7 +1143,7 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
|
|
1020
1143
|
return ScenarioList(new_scenarios)
|
1021
1144
|
|
1022
1145
|
@classmethod
|
1023
|
-
def from_list_of_tuples(self, *names: str, values: List[
|
1146
|
+
def from_list_of_tuples(self, *names: str, values: List[tuple]) -> ScenarioList:
|
1024
1147
|
sl = ScenarioList.from_list(names[0], [value[0] for value in values])
|
1025
1148
|
for index, name in enumerate(names[1:]):
|
1026
1149
|
sl = sl.add_list(name, [value[index + 1] for value in values])
|
@@ -1148,7 +1271,8 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
|
|
1148
1271
|
import sqlite3
|
1149
1272
|
|
1150
1273
|
if table is None and sql_query is None:
|
1151
|
-
|
1274
|
+
from .exceptions import ValueScenarioError
|
1275
|
+
raise ValueScenarioError("Either table or sql_query must be provided")
|
1152
1276
|
|
1153
1277
|
try:
|
1154
1278
|
with sqlite3.connect(filepath) as conn:
|
@@ -1210,12 +1334,12 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
|
|
1210
1334
|
"""
|
1211
1335
|
import tempfile
|
1212
1336
|
import requests
|
1213
|
-
from docx import Document
|
1214
1337
|
|
1215
1338
|
if "/edit" in url:
|
1216
1339
|
doc_id = url.split("/d/")[1].split("/edit")[0]
|
1217
1340
|
else:
|
1218
|
-
|
1341
|
+
from .exceptions import ValueScenarioError
|
1342
|
+
raise ValueScenarioError("Invalid Google Doc URL format.")
|
1219
1343
|
|
1220
1344
|
export_url = f"https://docs.google.com/document/d/{doc_id}/export?format=docx"
|
1221
1345
|
|
@@ -1419,7 +1543,8 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
|
|
1419
1543
|
print("The Excel file contains multiple sheets:")
|
1420
1544
|
for name in all_sheets.keys():
|
1421
1545
|
print(f"- {name}")
|
1422
|
-
|
1546
|
+
from .exceptions import ValueScenarioError
|
1547
|
+
raise ValueScenarioError("Please provide a sheet name to load data from.")
|
1423
1548
|
else:
|
1424
1549
|
# If there is only one sheet, use it
|
1425
1550
|
sheet_name = list(all_sheets.keys())[0]
|
@@ -1468,14 +1593,14 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
|
|
1468
1593
|
ScenarioList: An instance of the ScenarioList class.
|
1469
1594
|
|
1470
1595
|
"""
|
1471
|
-
import pandas as pd
|
1472
1596
|
import tempfile
|
1473
1597
|
import requests
|
1474
1598
|
|
1475
1599
|
if "/edit" in url:
|
1476
1600
|
sheet_id = url.split("/d/")[1].split("/edit")[0]
|
1477
1601
|
else:
|
1478
|
-
|
1602
|
+
from .exceptions import ValueScenarioError
|
1603
|
+
raise ValueScenarioError("Invalid Google Sheet URL format.")
|
1479
1604
|
|
1480
1605
|
export_url = (
|
1481
1606
|
f"https://docs.google.com/spreadsheets/d/{sheet_id}/export?format=xlsx"
|
@@ -1542,14 +1667,39 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
|
|
1542
1667
|
response.raise_for_status()
|
1543
1668
|
file_obj = StringIO(response.text)
|
1544
1669
|
else:
|
1545
|
-
|
1670
|
+
# Try different encodings if the default fails
|
1671
|
+
encodings_to_try = ["utf-8", "latin-1", "cp1252", "ISO-8859-1"]
|
1672
|
+
last_exception = None
|
1673
|
+
file_obj = None
|
1674
|
+
|
1675
|
+
for encoding in encodings_to_try:
|
1676
|
+
try:
|
1677
|
+
file_obj = open(source, "r", encoding=encoding)
|
1678
|
+
# Test reading a bit to verify encoding
|
1679
|
+
file_obj.readline()
|
1680
|
+
file_obj.seek(0) # Reset file position
|
1681
|
+
break
|
1682
|
+
except UnicodeDecodeError as e:
|
1683
|
+
last_exception = e
|
1684
|
+
if file_obj:
|
1685
|
+
file_obj.close()
|
1686
|
+
file_obj = None
|
1687
|
+
|
1688
|
+
if file_obj is None:
|
1689
|
+
from .exceptions import ValueScenarioError
|
1690
|
+
raise ValueScenarioError(f"Could not decode file {source} with any of the attempted encodings. Original error: {last_exception}")
|
1546
1691
|
|
1547
1692
|
reader = csv.reader(file_obj, delimiter=delimiter)
|
1548
|
-
|
1549
|
-
|
1693
|
+
try:
|
1694
|
+
header = next(reader)
|
1695
|
+
observations = [Scenario(dict(zip(header, row))) for row in reader]
|
1696
|
+
except StopIteration:
|
1697
|
+
from .exceptions import ValueScenarioError
|
1698
|
+
raise ValueScenarioError(f"File {source} appears to be empty or has an invalid format")
|
1550
1699
|
|
1551
1700
|
finally:
|
1552
|
-
file_obj
|
1701
|
+
if file_obj:
|
1702
|
+
file_obj.close()
|
1553
1703
|
|
1554
1704
|
return cls(observations)
|
1555
1705
|
|
@@ -1614,7 +1764,6 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
|
|
1614
1764
|
"""
|
1615
1765
|
from ..surveys import Survey
|
1616
1766
|
from ..questions import QuestionBase
|
1617
|
-
from ..jobs import Jobs
|
1618
1767
|
|
1619
1768
|
if isinstance(survey, QuestionBase):
|
1620
1769
|
return Survey([survey]).by(self)
|
@@ -1862,13 +2011,16 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
|
|
1862
2011
|
import string
|
1863
2012
|
|
1864
2013
|
if num_options < 2:
|
1865
|
-
|
2014
|
+
from .exceptions import ValueScenarioError
|
2015
|
+
raise ValueScenarioError("num_options must be at least 2")
|
1866
2016
|
|
1867
2017
|
if num_options > len(self):
|
1868
|
-
|
2018
|
+
from .exceptions import ValueScenarioError
|
2019
|
+
raise ValueScenarioError(f"num_options ({num_options}) cannot exceed the number of scenarios ({len(self)})")
|
1869
2020
|
|
1870
2021
|
if use_alphabet and num_options > 26:
|
1871
|
-
|
2022
|
+
from .exceptions import ValueScenarioError
|
2023
|
+
raise ValueScenarioError("When using alphabet labels, num_options cannot exceed 26 (the number of letters in the English alphabet)")
|
1872
2024
|
|
1873
2025
|
# Convert each scenario to a dictionary
|
1874
2026
|
scenario_dicts = [scenario.to_dict(add_edsl_version=False) for scenario in self]
|
edsl/surveys/__init__.py
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
from .survey import Survey
|
2
|
-
from .survey_flow_visualization import SurveyFlowVisualization
|
3
|
-
|
4
|
-
from .
|
5
|
-
from .base import EndOfSurvey, RulePriority
|
2
|
+
from .survey_flow_visualization import SurveyFlowVisualization # noqa: F401
|
3
|
+
from .rules import Rule, RuleCollection # noqa: F401
|
4
|
+
from .base import EndOfSurvey, RulePriority # noqa: F401
|
6
5
|
|
7
6
|
__all__ = ["Survey"]
|
8
7
|
## , "SurveyFlowVisualization", "Rule", "RuleCollection", "EndOfSurvey", "RulePriority"]
|
edsl/surveys/dag/__init__.py
CHANGED
edsl/surveys/descriptors.py
CHANGED
edsl/surveys/edit_survey.py
CHANGED
@@ -5,6 +5,7 @@ from typing import Union, Optional, TYPE_CHECKING
|
|
5
5
|
if TYPE_CHECKING:
|
6
6
|
from ..questions import QuestionBase
|
7
7
|
from .survey import Survey
|
8
|
+
from ..instructions import Instruction, ChangeInstruction
|
8
9
|
|
9
10
|
from .exceptions import SurveyError, SurveyCreationError
|
10
11
|
from .rules.rule import Rule
|
edsl/surveys/exceptions.py
CHANGED
@@ -1,36 +1,192 @@
|
|
1
1
|
from ..base import BaseException
|
2
2
|
|
3
3
|
class SurveyError(BaseException):
|
4
|
-
|
4
|
+
"""
|
5
|
+
Base exception class for all survey-related errors.
|
6
|
+
|
7
|
+
This exception is the parent class for all exceptions related to Survey operations,
|
8
|
+
including creation, validation, and navigation. It provides a common type
|
9
|
+
for catching any survey-specific error.
|
10
|
+
|
11
|
+
This exception is raised directly when:
|
12
|
+
- Question names don't meet validation requirements
|
13
|
+
- Survey operations encounter general errors not covered by more specific exceptions
|
14
|
+
"""
|
15
|
+
doc_page = "surveys"
|
5
16
|
|
6
17
|
|
7
18
|
class SurveyCreationError(SurveyError):
|
8
|
-
|
19
|
+
"""
|
20
|
+
Exception raised when there's an error creating or modifying a survey.
|
21
|
+
|
22
|
+
This exception occurs when:
|
23
|
+
- Adding skip rules to EndOfSurvey (which isn't allowed)
|
24
|
+
- Combining surveys with non-default rules
|
25
|
+
- Adding questions with duplicate names
|
26
|
+
- Creating invalid question groups
|
27
|
+
- Validating group names and boundaries
|
28
|
+
|
29
|
+
To fix this error:
|
30
|
+
1. Ensure all question names in a survey are unique
|
31
|
+
2. Don't add skip rules to EndOfSurvey
|
32
|
+
3. Check that question groups are properly defined
|
33
|
+
4. When combining surveys, ensure they're compatible
|
34
|
+
|
35
|
+
Examples:
|
36
|
+
```python
|
37
|
+
survey.add(question) # Raises SurveyCreationError if question's name already exists
|
38
|
+
survey.add_rules_to_question(EndOfSurvey) # Raises SurveyCreationError
|
39
|
+
```
|
40
|
+
"""
|
41
|
+
doc_anchor = "creating-surveys"
|
9
42
|
|
10
43
|
|
11
44
|
class SurveyHasNoRulesError(SurveyError):
|
12
|
-
|
45
|
+
"""
|
46
|
+
Exception raised when rules are required but not found for a question.
|
47
|
+
|
48
|
+
This exception occurs when:
|
49
|
+
- The survey's next_question method is called but no rules exist for the current question
|
50
|
+
- Navigation can't proceed because there's no defined path forward
|
51
|
+
|
52
|
+
To fix this error:
|
53
|
+
1. Add appropriate rules to all questions in the survey
|
54
|
+
2. Ensure the survey has a complete navigation path from start to finish
|
55
|
+
3. Use default rules where appropriate (add_default_rules method)
|
56
|
+
|
57
|
+
Examples:
|
58
|
+
```python
|
59
|
+
survey.next_question(0) # Raises SurveyHasNoRulesError if question 0 has no rules
|
60
|
+
```
|
61
|
+
"""
|
62
|
+
relevant_doc = "https://docs.expectedparrot.com/en/latest/surveys.html#rules"
|
13
63
|
|
14
64
|
|
15
65
|
class SurveyRuleSendsYouBackwardsError(SurveyError):
|
16
|
-
|
66
|
+
"""
|
67
|
+
Exception raised when a rule would navigate backward in the survey flow.
|
68
|
+
|
69
|
+
This exception occurs during rule initialization to prevent rules that
|
70
|
+
would navigate backward in the survey flow, which is not allowed.
|
71
|
+
|
72
|
+
Backward navigation creates potential loops and is generally considered
|
73
|
+
poor survey design. EDSL enforces forward-only navigation.
|
74
|
+
|
75
|
+
To fix this error:
|
76
|
+
1. Redesign your survey to avoid backward navigation
|
77
|
+
2. Use forward-only rules with proper branching logic
|
78
|
+
3. Consider using memory to carry forward information if needed
|
79
|
+
|
80
|
+
Examples:
|
81
|
+
```python
|
82
|
+
survey.add_rule(question_index=2, rule=Rule(lambda x: True, 1)) # Raises SurveyRuleSendsYouBackwardsError
|
83
|
+
```
|
84
|
+
"""
|
85
|
+
relevant_doc = "https://docs.expectedparrot.com/en/latest/surveys.html#rules"
|
17
86
|
|
18
87
|
|
19
88
|
class SurveyRuleSkipLogicSyntaxError(SurveyError):
|
20
|
-
|
89
|
+
"""
|
90
|
+
Exception raised when a rule's expression has invalid syntax.
|
91
|
+
|
92
|
+
This exception occurs when:
|
93
|
+
- The expression in a rule is not valid Python syntax
|
94
|
+
- The expression can't be parsed or compiled
|
95
|
+
|
96
|
+
To fix this error:
|
97
|
+
1. Check the syntax of your rule expression
|
98
|
+
2. Ensure all variables in the expression are properly referenced
|
99
|
+
3. Test the expression in isolation to verify it's valid Python
|
100
|
+
|
101
|
+
Examples:
|
102
|
+
```python
|
103
|
+
Rule(lambda x: x[question] ==, 1) # Raises SurveyRuleSkipLogicSyntaxError (invalid syntax)
|
104
|
+
```
|
105
|
+
"""
|
106
|
+
relevant_doc = "https://docs.expectedparrot.com/en/latest/surveys.html#rules"
|
21
107
|
|
22
108
|
|
23
109
|
class SurveyRuleReferenceInRuleToUnknownQuestionError(SurveyError):
|
24
|
-
|
110
|
+
"""
|
111
|
+
Exception raised when a rule references an unknown question.
|
112
|
+
|
113
|
+
This exception is designed to catch cases where a rule's condition
|
114
|
+
references a question that doesn't exist in the survey.
|
115
|
+
|
116
|
+
To fix this error:
|
117
|
+
1. Ensure all questions referenced in rule conditions exist in the survey
|
118
|
+
2. Check for typos in question names or indices
|
119
|
+
|
120
|
+
Note: This exception is defined but not actively used in the codebase.
|
121
|
+
It raises Exception("not used") to indicate this state.
|
122
|
+
"""
|
123
|
+
def __init__(self, message="Rule references an unknown question", **kwargs):
|
124
|
+
super().__init__(message, **kwargs)
|
25
125
|
|
26
126
|
|
27
127
|
class SurveyRuleRefersToFutureStateError(SurveyError):
|
28
|
-
|
128
|
+
"""
|
129
|
+
Exception raised when a rule references questions that come later in the survey.
|
130
|
+
|
131
|
+
This exception occurs when:
|
132
|
+
- A rule condition refers to questions that haven't been presented yet
|
133
|
+
- Rule evaluation would require information not yet collected
|
134
|
+
|
135
|
+
To fix this error:
|
136
|
+
1. Redesign your rules to only reference current or previous questions
|
137
|
+
2. Ensure rule conditions only depend on information already collected
|
138
|
+
3. Restructure your survey if you need different branching logic
|
139
|
+
|
140
|
+
Examples:
|
141
|
+
```python
|
142
|
+
# If question 3 hasn't been asked yet:
|
143
|
+
Rule(lambda x: x['q3_answer'] == 'Yes', next_question=4) # Raises SurveyRuleRefersToFutureStateError
|
144
|
+
```
|
145
|
+
"""
|
146
|
+
relevant_doc = "https://docs.expectedparrot.com/en/latest/surveys.html#rules"
|
29
147
|
|
30
148
|
|
31
149
|
class SurveyRuleCollectionHasNoRulesAtNodeError(SurveyError):
|
32
|
-
|
150
|
+
"""
|
151
|
+
Exception raised when no rules are found for a specific question during navigation.
|
152
|
+
|
153
|
+
This exception occurs when:
|
154
|
+
- The RuleCollection's next_question method can't find applicable rules
|
155
|
+
- A survey is trying to determine the next question but has no rule for the current state
|
156
|
+
|
157
|
+
To fix this error:
|
158
|
+
1. Add rules for all questions in your survey
|
159
|
+
2. Ensure rules are properly added to the RuleCollection
|
160
|
+
3. Add default rules where appropriate
|
161
|
+
|
162
|
+
Examples:
|
163
|
+
```python
|
164
|
+
# If rule_collection has no rules for question 2:
|
165
|
+
rule_collection.next_question(2, {}) # Raises SurveyRuleCollectionHasNoRulesAtNodeError
|
166
|
+
```
|
167
|
+
"""
|
168
|
+
relevant_doc = "https://docs.expectedparrot.com/en/latest/surveys.html#rule-collections"
|
33
169
|
|
34
170
|
|
35
171
|
class SurveyRuleCannotEvaluateError(SurveyError):
|
36
|
-
|
172
|
+
"""
|
173
|
+
Exception raised when a rule expression cannot be evaluated.
|
174
|
+
|
175
|
+
This exception occurs when:
|
176
|
+
- The rule's expression fails to evaluate with the provided data
|
177
|
+
- Required variables are missing in the evaluation context
|
178
|
+
- The expression contains errors that only appear at runtime
|
179
|
+
|
180
|
+
To fix this error:
|
181
|
+
1. Check that your rule expression is valid
|
182
|
+
2. Ensure all referenced variables are available in the context
|
183
|
+
3. Add error handling in complex expressions
|
184
|
+
4. Test rules with sample data before using in production
|
185
|
+
|
186
|
+
Examples:
|
187
|
+
```python
|
188
|
+
# If 'q1_answer' is not in the data dictionary:
|
189
|
+
Rule(lambda x: x['q1_answer'] == 'Yes', 2).evaluate({}) # Raises SurveyRuleCannotEvaluateError
|
190
|
+
```
|
191
|
+
"""
|
192
|
+
relevant_doc = "https://docs.expectedparrot.com/en/latest/surveys.html#rules"
|
edsl/surveys/memory/__init__.py
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
-
from .memory_plan import MemoryPlan
|
2
|
-
from .memory_management import MemoryManagement
|
3
|
-
from .memory import Memory
|
1
|
+
from .memory_plan import MemoryPlan # noqa: F401
|
2
|
+
from .memory_management import MemoryManagement # noqa: F401
|
3
|
+
from .memory import Memory # noqa: F401
|
4
|
+
|
5
|
+
__all__ = []
|