edsl 0.1.60__py3-none-any.whl → 0.1.61__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/__version__.py +1 -1
- edsl/agents/agent.py +65 -17
- edsl/agents/agent_list.py +117 -33
- edsl/base/base_class.py +80 -11
- edsl/config/config_class.py +7 -2
- edsl/coop/coop.py +1295 -85
- edsl/coop/coop_prolific_filters.py +171 -0
- edsl/dataset/display/table_display.py +40 -7
- edsl/db_list/sqlite_list.py +102 -3
- edsl/jobs/data_structures.py +46 -31
- edsl/jobs/jobs.py +73 -2
- edsl/jobs/remote_inference.py +49 -15
- edsl/questions/loop_processor.py +289 -10
- edsl/questions/templates/dict/answering_instructions.jinja +0 -1
- edsl/scenarios/scenario_list.py +31 -1
- edsl/scenarios/scenario_source.py +606 -498
- edsl/surveys/survey.py +198 -163
- {edsl-0.1.60.dist-info → edsl-0.1.61.dist-info}/METADATA +3 -3
- {edsl-0.1.60.dist-info → edsl-0.1.61.dist-info}/RECORD +22 -21
- {edsl-0.1.60.dist-info → edsl-0.1.61.dist-info}/LICENSE +0 -0
- {edsl-0.1.60.dist-info → edsl-0.1.61.dist-info}/WHEEL +0 -0
- {edsl-0.1.60.dist-info → edsl-0.1.61.dist-info}/entry_points.txt +0 -0
edsl/surveys/survey.py
CHANGED
@@ -43,8 +43,8 @@ if TYPE_CHECKING:
|
|
43
43
|
from ..scenarios import ScenarioList
|
44
44
|
from ..buckets.bucket_collection import BucketCollection
|
45
45
|
from ..key_management.key_lookup import KeyLookup
|
46
|
-
|
47
|
-
# Define types for documentation purpose only
|
46
|
+
|
47
|
+
# Define types for documentation purpose only
|
48
48
|
VisibilityType = Literal["unlisted", "public", "private"]
|
49
49
|
Table = Any # Type for table display
|
50
50
|
# Type alias for docx document
|
@@ -70,24 +70,26 @@ from .rules import RuleManager, RuleCollection
|
|
70
70
|
from .survey_export import SurveyExport
|
71
71
|
from .exceptions import SurveyCreationError, SurveyHasNoRulesError, SurveyError
|
72
72
|
|
73
|
+
|
73
74
|
class PseudoIndices(UserDict):
|
74
75
|
"""A dictionary of pseudo-indices for the survey.
|
75
|
-
|
76
|
+
|
76
77
|
This class manages indices for both questions and instructions in a survey. It assigns
|
77
78
|
floating-point indices to instructions so they can be interspersed between integer-indexed
|
78
79
|
questions while maintaining order. This is crucial for properly serializing and deserializing
|
79
80
|
surveys with both questions and instructions.
|
80
|
-
|
81
|
+
|
81
82
|
Attributes:
|
82
83
|
data (dict): The underlying dictionary mapping item names to their pseudo-indices.
|
83
84
|
"""
|
85
|
+
|
84
86
|
@property
|
85
87
|
def max_pseudo_index(self) -> float:
|
86
88
|
"""Return the maximum pseudo index in the survey.
|
87
|
-
|
89
|
+
|
88
90
|
Returns:
|
89
91
|
float: The highest pseudo-index value currently assigned, or -1 if empty.
|
90
|
-
|
92
|
+
|
91
93
|
Examples:
|
92
94
|
>>> Survey.example()._pseudo_indices.max_pseudo_index
|
93
95
|
2
|
@@ -103,7 +105,7 @@ class PseudoIndices(UserDict):
|
|
103
105
|
This is used to determine the pseudo-index of the next item added to the survey.
|
104
106
|
Instructions are assigned floating-point indices (e.g., 1.5) while questions
|
105
107
|
have integer indices.
|
106
|
-
|
108
|
+
|
107
109
|
Returns:
|
108
110
|
bool: True if the last added item was an instruction, False otherwise.
|
109
111
|
|
@@ -121,21 +123,21 @@ class PseudoIndices(UserDict):
|
|
121
123
|
|
122
124
|
class Survey(Base):
|
123
125
|
"""A collection of questions with logic for navigating between them.
|
124
|
-
|
126
|
+
|
125
127
|
Survey is the main class for creating, modifying, and running surveys. It supports:
|
126
|
-
|
128
|
+
|
127
129
|
- Skip logic: conditional navigation between questions based on previous answers
|
128
130
|
- Memory: controlling which previous answers are visible to agents
|
129
131
|
- Question grouping: organizing questions into logical sections
|
130
132
|
- Randomization: randomly ordering certain questions to reduce bias
|
131
133
|
- Instructions: adding non-question elements to guide respondents
|
132
|
-
|
134
|
+
|
133
135
|
A Survey instance can be used to:
|
134
136
|
1. Define a set of questions and their order
|
135
137
|
2. Add rules for navigating between questions
|
136
138
|
3. Run the survey with agents or humans
|
137
139
|
4. Export the survey in various formats
|
138
|
-
|
140
|
+
|
139
141
|
The survey maintains the order of questions, any skip logic rules, and handles
|
140
142
|
serialization for storage or transmission.
|
141
143
|
"""
|
@@ -187,15 +189,15 @@ class Survey(Base):
|
|
187
189
|
|
188
190
|
Examples:
|
189
191
|
Create a basic survey with three questions:
|
190
|
-
|
192
|
+
|
191
193
|
>>> from edsl import QuestionFreeText
|
192
194
|
>>> q1 = QuestionFreeText(question_text="What is your name?", question_name="name")
|
193
195
|
>>> q2 = QuestionFreeText(question_text="What is your favorite color?", question_name="color")
|
194
196
|
>>> q3 = QuestionFreeText(question_text="Is a hot dog a sandwich?", question_name="food")
|
195
197
|
>>> s = Survey([q1, q2, q3])
|
196
|
-
|
198
|
+
|
197
199
|
Create a survey with question groups:
|
198
|
-
|
200
|
+
|
199
201
|
>>> s = Survey([q1, q2, q3], question_groups={"demographics": (0, 1), "food_questions": (2, 2)})
|
200
202
|
"""
|
201
203
|
|
@@ -240,12 +242,12 @@ class Survey(Base):
|
|
240
242
|
|
241
243
|
self._exporter = SurveyExport(self)
|
242
244
|
|
243
|
-
|
244
245
|
# In survey.py
|
245
246
|
@property
|
246
247
|
def ep(self):
|
247
248
|
"""Return plugin host for this survey."""
|
248
249
|
from ..plugins.plugin_host import PluginHost
|
250
|
+
|
249
251
|
return PluginHost(self)
|
250
252
|
|
251
253
|
def question_names_valid(self) -> bool:
|
@@ -276,9 +278,13 @@ class Survey(Base):
|
|
276
278
|
"""Process the raw questions passed to the survey."""
|
277
279
|
handler = InstructionHandler(self)
|
278
280
|
result = handler.separate_questions_and_instructions(questions or [])
|
279
|
-
|
281
|
+
|
280
282
|
# Handle result safely for mypy
|
281
|
-
if
|
283
|
+
if (
|
284
|
+
hasattr(result, "true_questions")
|
285
|
+
and hasattr(result, "instruction_names_to_instructions")
|
286
|
+
and hasattr(result, "pseudo_indices")
|
287
|
+
):
|
282
288
|
# It's the SeparatedComponents dataclass
|
283
289
|
self._instruction_names_to_instructions = result.instruction_names_to_instructions # type: ignore
|
284
290
|
self._pseudo_indices = PseudoIndices(result.pseudo_indices) # type: ignore
|
@@ -295,7 +301,9 @@ class Survey(Base):
|
|
295
301
|
self._pseudo_indices = PseudoIndices(pseudo_idx)
|
296
302
|
return true_q
|
297
303
|
else:
|
298
|
-
raise TypeError(
|
304
|
+
raise TypeError(
|
305
|
+
f"Unexpected result type from separate_questions_and_instructions: {type(result)}"
|
306
|
+
)
|
299
307
|
|
300
308
|
@property
|
301
309
|
def _relevant_instructions_dict(self) -> InstructionCollection:
|
@@ -384,7 +392,7 @@ class Survey(Base):
|
|
384
392
|
if question_name not in self.question_name_to_index:
|
385
393
|
raise SurveyError(f"Question name {question_name} not found in survey.")
|
386
394
|
return self.questions[self.question_name_to_index[question_name]]
|
387
|
-
|
395
|
+
|
388
396
|
def get(self, question_name: str) -> QuestionBase:
|
389
397
|
"""Return the question object given the question name."""
|
390
398
|
return self._get_question_by_name(question_name)
|
@@ -417,22 +425,32 @@ class Survey(Base):
|
|
417
425
|
"""
|
418
426
|
return {q.question_name: i for i, q in enumerate(self.questions)}
|
419
427
|
|
428
|
+
def to_long_format(
|
429
|
+
self, scenario_list: ScenarioList
|
430
|
+
) -> Tuple[List[QuestionBase], ScenarioList]:
|
431
|
+
"""Return a new survey with the questions in long format and the associated scenario list."""
|
432
|
+
|
433
|
+
from edsl.questions.loop_processor import LongSurveyLoopProcessor
|
434
|
+
|
435
|
+
lp = LongSurveyLoopProcessor(self, scenario_list)
|
436
|
+
return lp.process_templates_for_all_questions()
|
437
|
+
|
420
438
|
def to_dict(self, add_edsl_version=True) -> dict[str, Any]:
|
421
439
|
"""Serialize the Survey object to a dictionary for storage or transmission.
|
422
|
-
|
440
|
+
|
423
441
|
This method converts the entire survey structure, including questions, rules,
|
424
442
|
memory plan, and question groups, into a dictionary that can be serialized to JSON.
|
425
443
|
This is essential for saving surveys, sharing them, or transferring them between
|
426
444
|
systems.
|
427
|
-
|
445
|
+
|
428
446
|
The serialized dictionary contains the complete state of the survey, allowing it
|
429
447
|
to be fully reconstructed using the from_dict() method.
|
430
|
-
|
448
|
+
|
431
449
|
Args:
|
432
450
|
add_edsl_version: If True (default), includes the EDSL version and class name
|
433
451
|
in the dictionary, which can be useful for backward compatibility when
|
434
452
|
deserializing.
|
435
|
-
|
453
|
+
|
436
454
|
Returns:
|
437
455
|
dict[str, Any]: A dictionary representation of the survey with the following keys:
|
438
456
|
- 'questions': List of serialized questions and instructions
|
@@ -442,14 +460,14 @@ class Survey(Base):
|
|
442
460
|
- 'questions_to_randomize': List of questions to randomize (if any)
|
443
461
|
- 'edsl_version': EDSL version (if add_edsl_version=True)
|
444
462
|
- 'edsl_class_name': Class name (if add_edsl_version=True)
|
445
|
-
|
463
|
+
|
446
464
|
Examples:
|
447
465
|
>>> s = Survey.example()
|
448
466
|
>>> s.to_dict(add_edsl_version=False).keys()
|
449
467
|
dict_keys(['questions', 'memory_plan', 'rule_collection', 'question_groups'])
|
450
|
-
|
468
|
+
|
451
469
|
With version information:
|
452
|
-
|
470
|
+
|
453
471
|
>>> d = s.to_dict(add_edsl_version=True)
|
454
472
|
>>> 'edsl_version' in d and 'edsl_class_name' in d
|
455
473
|
True
|
@@ -468,7 +486,7 @@ class Survey(Base):
|
|
468
486
|
),
|
469
487
|
"question_groups": self.question_groups,
|
470
488
|
}
|
471
|
-
|
489
|
+
|
472
490
|
# Include randomization information if present
|
473
491
|
if self.questions_to_randomize != []:
|
474
492
|
d["questions_to_randomize"] = self.questions_to_randomize
|
@@ -477,45 +495,46 @@ class Survey(Base):
|
|
477
495
|
if add_edsl_version:
|
478
496
|
d["edsl_version"] = __version__
|
479
497
|
d["edsl_class_name"] = "Survey"
|
480
|
-
|
498
|
+
|
481
499
|
return d
|
482
500
|
|
483
501
|
@classmethod
|
484
502
|
@remove_edsl_version
|
485
503
|
def from_dict(cls, data: dict) -> Survey:
|
486
504
|
"""Reconstruct a Survey object from its dictionary representation.
|
487
|
-
|
505
|
+
|
488
506
|
This class method is the counterpart to to_dict() and allows you to recreate
|
489
507
|
a Survey object from a serialized dictionary. This is useful for loading saved
|
490
508
|
surveys, receiving surveys from other systems, or cloning surveys.
|
491
|
-
|
509
|
+
|
492
510
|
The method handles deserialization of all survey components, including questions,
|
493
511
|
instructions, memory plan, rules, and question groups.
|
494
|
-
|
512
|
+
|
495
513
|
Args:
|
496
514
|
data: A dictionary containing the serialized survey data, typically
|
497
515
|
created by the to_dict() method.
|
498
|
-
|
516
|
+
|
499
517
|
Returns:
|
500
518
|
Survey: A fully reconstructed Survey object with all the original
|
501
519
|
questions, rules, and configuration.
|
502
|
-
|
520
|
+
|
503
521
|
Examples:
|
504
522
|
Create a survey, serialize it, and deserialize it back:
|
505
|
-
|
523
|
+
|
506
524
|
>>> d = Survey.example().to_dict()
|
507
525
|
>>> s = Survey.from_dict(d)
|
508
526
|
>>> s == Survey.example()
|
509
527
|
True
|
510
|
-
|
528
|
+
|
511
529
|
Works with instructions as well:
|
512
|
-
|
530
|
+
|
513
531
|
>>> s = Survey.example(include_instructions=True)
|
514
532
|
>>> d = s.to_dict()
|
515
533
|
>>> news = Survey.from_dict(d)
|
516
534
|
>>> news == s
|
517
535
|
True
|
518
536
|
"""
|
537
|
+
|
519
538
|
# Helper function to determine the correct class for each serialized component
|
520
539
|
def get_class(pass_dict):
|
521
540
|
from ..questions import QuestionBase
|
@@ -524,12 +543,15 @@ class Survey(Base):
|
|
524
543
|
return QuestionBase
|
525
544
|
elif pass_dict.get("edsl_class_name") == "QuestionDict":
|
526
545
|
from ..questions import QuestionDict
|
546
|
+
|
527
547
|
return QuestionDict
|
528
548
|
elif class_name == "Instruction":
|
529
549
|
from ..instructions import Instruction
|
550
|
+
|
530
551
|
return Instruction
|
531
552
|
elif class_name == "ChangeInstruction":
|
532
553
|
from ..instructions import ChangeInstruction
|
554
|
+
|
533
555
|
return ChangeInstruction
|
534
556
|
else:
|
535
557
|
return QuestionBase
|
@@ -538,16 +560,16 @@ class Survey(Base):
|
|
538
560
|
questions = [
|
539
561
|
get_class(q_dict).from_dict(q_dict) for q_dict in data["questions"]
|
540
562
|
]
|
541
|
-
|
563
|
+
|
542
564
|
# Deserialize the memory plan
|
543
565
|
memory_plan = MemoryPlan.from_dict(data["memory_plan"])
|
544
|
-
|
566
|
+
|
545
567
|
# Get the list of questions to randomize if present
|
546
568
|
if "questions_to_randomize" in data:
|
547
569
|
questions_to_randomize = data["questions_to_randomize"]
|
548
570
|
else:
|
549
571
|
questions_to_randomize = None
|
550
|
-
|
572
|
+
|
551
573
|
# Create and return the reconstructed survey
|
552
574
|
survey = cls(
|
553
575
|
questions=questions,
|
@@ -693,14 +715,14 @@ class Survey(Base):
|
|
693
715
|
|
694
716
|
def set_full_memory_mode(self) -> Survey:
|
695
717
|
"""Configure the survey so agents remember all previous questions and answers.
|
696
|
-
|
718
|
+
|
697
719
|
In full memory mode, when an agent answers any question, it will have access to
|
698
720
|
all previously asked questions and the agent's answers to them. This is useful
|
699
721
|
for surveys where later questions build on or reference earlier responses.
|
700
|
-
|
722
|
+
|
701
723
|
Returns:
|
702
724
|
Survey: The modified survey instance (allows for method chaining).
|
703
|
-
|
725
|
+
|
704
726
|
Examples:
|
705
727
|
>>> s = Survey.example().set_full_memory_mode()
|
706
728
|
"""
|
@@ -709,21 +731,21 @@ class Survey(Base):
|
|
709
731
|
|
710
732
|
def set_lagged_memory(self, lags: int) -> Survey:
|
711
733
|
"""Configure the survey so agents remember a limited window of previous questions.
|
712
|
-
|
734
|
+
|
713
735
|
In lagged memory mode, when an agent answers a question, it will only have access
|
714
736
|
to the most recent 'lags' number of questions and answers. This is useful for
|
715
737
|
limiting context when only recent questions are relevant.
|
716
|
-
|
738
|
+
|
717
739
|
Args:
|
718
740
|
lags: The number of previous questions to remember. For example, if lags=2,
|
719
741
|
only the two most recent questions and answers will be remembered.
|
720
|
-
|
742
|
+
|
721
743
|
Returns:
|
722
744
|
Survey: The modified survey instance (allows for method chaining).
|
723
|
-
|
745
|
+
|
724
746
|
Examples:
|
725
747
|
Remember only the two most recent questions:
|
726
|
-
|
748
|
+
|
727
749
|
>>> s = Survey.example().set_lagged_memory(2)
|
728
750
|
"""
|
729
751
|
MemoryManagement(self)._set_memory_plan(
|
@@ -733,15 +755,15 @@ class Survey(Base):
|
|
733
755
|
|
734
756
|
def _set_memory_plan(self, prior_questions_func: Callable) -> None:
|
735
757
|
"""Set a custom memory plan based on a provided function.
|
736
|
-
|
758
|
+
|
737
759
|
This is an internal method used to define custom memory plans. The function
|
738
760
|
provided determines which previous questions should be remembered for each
|
739
761
|
question index.
|
740
|
-
|
762
|
+
|
741
763
|
Args:
|
742
764
|
prior_questions_func: A function that takes the index of the current question
|
743
765
|
and returns a list of question names to remember.
|
744
|
-
|
766
|
+
|
745
767
|
Examples:
|
746
768
|
>>> s = Survey.example()
|
747
769
|
>>> s._set_memory_plan(lambda i: s.question_names[:i])
|
@@ -754,23 +776,23 @@ class Survey(Base):
|
|
754
776
|
prior_question: Union[QuestionBase, str],
|
755
777
|
) -> Survey:
|
756
778
|
"""Configure the survey so a specific question has access to a prior question's answer.
|
757
|
-
|
779
|
+
|
758
780
|
This method allows you to define memory relationships between specific questions.
|
759
781
|
When an agent answers the focal_question, it will have access to the prior_question
|
760
782
|
and its answer, regardless of other memory settings.
|
761
|
-
|
783
|
+
|
762
784
|
Args:
|
763
785
|
focal_question: The question for which to add memory, specified either as a
|
764
786
|
QuestionBase object or its question_name string.
|
765
787
|
prior_question: The prior question to remember, specified either as a
|
766
788
|
QuestionBase object or its question_name string.
|
767
|
-
|
789
|
+
|
768
790
|
Returns:
|
769
791
|
Survey: The modified survey instance (allows for method chaining).
|
770
|
-
|
792
|
+
|
771
793
|
Examples:
|
772
794
|
When answering q2, remember the answer to q0:
|
773
|
-
|
795
|
+
|
774
796
|
>>> s = Survey.example().add_targeted_memory("q2", "q0")
|
775
797
|
>>> s.memory_plan
|
776
798
|
{'q2': Memory(prior_questions=['q0'])}
|
@@ -785,23 +807,23 @@ class Survey(Base):
|
|
785
807
|
prior_questions: List[Union[QuestionBase, str]],
|
786
808
|
) -> Survey:
|
787
809
|
"""Configure the survey so a specific question has access to multiple prior questions.
|
788
|
-
|
810
|
+
|
789
811
|
This method allows you to define memory relationships between specific questions.
|
790
812
|
When an agent answers the focal_question, it will have access to all the questions
|
791
813
|
and answers specified in prior_questions.
|
792
|
-
|
814
|
+
|
793
815
|
Args:
|
794
816
|
focal_question: The question for which to add memory, specified either as a
|
795
817
|
QuestionBase object or its question_name string.
|
796
818
|
prior_questions: A list of prior questions to remember, each specified either
|
797
819
|
as a QuestionBase object or its question_name string.
|
798
|
-
|
820
|
+
|
799
821
|
Returns:
|
800
822
|
Survey: The modified survey instance (allows for method chaining).
|
801
|
-
|
823
|
+
|
802
824
|
Examples:
|
803
825
|
When answering q2, remember the answers to both q0 and q1:
|
804
|
-
|
826
|
+
|
805
827
|
>>> s = Survey.example().add_memory_collection("q2", ["q0", "q1"])
|
806
828
|
>>> s.memory_plan
|
807
829
|
{'q2': Memory(prior_questions=['q0', 'q1'])}
|
@@ -817,16 +839,16 @@ class Survey(Base):
|
|
817
839
|
group_name: str,
|
818
840
|
) -> Survey:
|
819
841
|
"""Create a logical group of questions within the survey.
|
820
|
-
|
842
|
+
|
821
843
|
Question groups allow you to organize questions into meaningful sections,
|
822
844
|
which can be useful for:
|
823
845
|
- Analysis (analyzing responses by section)
|
824
846
|
- Navigation (jumping between sections)
|
825
847
|
- Presentation (displaying sections with headers)
|
826
|
-
|
848
|
+
|
827
849
|
Groups are defined by a contiguous range of questions from start_question
|
828
850
|
to end_question, inclusive. Groups cannot overlap with other groups.
|
829
|
-
|
851
|
+
|
830
852
|
Args:
|
831
853
|
start_question: The first question in the group, specified either as a
|
832
854
|
QuestionBase object or its question_name string.
|
@@ -834,24 +856,24 @@ class Survey(Base):
|
|
834
856
|
QuestionBase object or its question_name string.
|
835
857
|
group_name: A name for the group. Must be a valid Python identifier
|
836
858
|
and must not conflict with existing group or question names.
|
837
|
-
|
859
|
+
|
838
860
|
Returns:
|
839
861
|
Survey: The modified survey instance (allows for method chaining).
|
840
|
-
|
862
|
+
|
841
863
|
Raises:
|
842
864
|
SurveyCreationError: If the group name is invalid, already exists,
|
843
865
|
conflicts with a question name, if start comes after end,
|
844
866
|
or if the group overlaps with an existing group.
|
845
|
-
|
867
|
+
|
846
868
|
Examples:
|
847
869
|
Create a group of questions for demographics:
|
848
|
-
|
870
|
+
|
849
871
|
>>> s = Survey.example().add_question_group("q0", "q1", "group1")
|
850
872
|
>>> s.question_groups
|
851
873
|
{'group1': (0, 1)}
|
852
|
-
|
874
|
+
|
853
875
|
Group names must be valid Python identifiers:
|
854
|
-
|
876
|
+
|
855
877
|
>>> from edsl.surveys.exceptions import SurveyCreationError
|
856
878
|
>>> # Example showing invalid group name error
|
857
879
|
>>> try:
|
@@ -859,18 +881,18 @@ class Survey(Base):
|
|
859
881
|
... except SurveyCreationError:
|
860
882
|
... print("Error: Invalid group name (as expected)")
|
861
883
|
Error: Invalid group name (as expected)
|
862
|
-
|
884
|
+
|
863
885
|
Group names can't conflict with question names:
|
864
|
-
|
886
|
+
|
865
887
|
>>> # Example showing name conflict error
|
866
888
|
>>> try:
|
867
889
|
... Survey.example().add_question_group("q0", "q1", "q0")
|
868
890
|
... except SurveyCreationError:
|
869
891
|
... print("Error: Group name conflicts with question name (as expected)")
|
870
892
|
Error: Group name conflicts with question name (as expected)
|
871
|
-
|
893
|
+
|
872
894
|
Start question must come before end question:
|
873
|
-
|
895
|
+
|
874
896
|
>>> # Example showing index order error
|
875
897
|
>>> try:
|
876
898
|
... Survey.example().add_question_group("q1", "q0", "group1")
|
@@ -902,20 +924,23 @@ class Survey(Base):
|
|
902
924
|
raise SurveyCreationError(
|
903
925
|
"Cannot use EndOfSurvey as a boundary for question groups."
|
904
926
|
)
|
905
|
-
|
927
|
+
|
906
928
|
# Now we know both are integers
|
907
929
|
assert isinstance(start_index, int) and isinstance(end_index, int)
|
908
|
-
|
930
|
+
|
909
931
|
if start_index > end_index:
|
910
932
|
raise SurveyCreationError(
|
911
933
|
f"Start index {start_index} is greater than end index {end_index}."
|
912
934
|
)
|
913
935
|
|
914
936
|
# Check for overlaps with existing groups
|
915
|
-
for existing_group_name, (
|
937
|
+
for existing_group_name, (
|
938
|
+
exist_start,
|
939
|
+
exist_end,
|
940
|
+
) in self.question_groups.items():
|
916
941
|
# Ensure the existing indices are integers (they should be, but for type checking)
|
917
942
|
assert isinstance(exist_start, int) and isinstance(exist_end, int)
|
918
|
-
|
943
|
+
|
919
944
|
# Check containment and overlap cases
|
920
945
|
if start_index < exist_start and end_index > exist_end:
|
921
946
|
raise SurveyCreationError(
|
@@ -989,7 +1014,7 @@ class Survey(Base):
|
|
989
1014
|
self, question: Union["QuestionBase", str], expression: str
|
990
1015
|
) -> Survey:
|
991
1016
|
"""Add a rule to skip a question based on a conditional expression.
|
992
|
-
|
1017
|
+
|
993
1018
|
Skip rules are evaluated *before* the question is presented. If the expression
|
994
1019
|
evaluates to True, the question is skipped and the flow proceeds to the next
|
995
1020
|
question in sequence. This is different from jump rules which are evaluated
|
@@ -1001,7 +1026,7 @@ class Survey(Base):
|
|
1001
1026
|
expression: A string expression that will be evaluated to determine if the
|
1002
1027
|
question should be skipped. Can reference previous questions' answers
|
1003
1028
|
using the template syntax, e.g., "{{ q0.answer }} == 'yes'".
|
1004
|
-
|
1029
|
+
|
1005
1030
|
Returns:
|
1006
1031
|
Survey: The modified survey instance (allows for method chaining).
|
1007
1032
|
|
@@ -1016,16 +1041,16 @@ class Survey(Base):
|
|
1016
1041
|
>>> s = Survey([q0, q1]).add_skip_rule("q0", "True")
|
1017
1042
|
>>> s.next_question("q0", {}).question_name
|
1018
1043
|
'q1'
|
1019
|
-
|
1044
|
+
|
1020
1045
|
Skip a question conditionally:
|
1021
|
-
|
1046
|
+
|
1022
1047
|
>>> q2 = QuestionFreeText.example()
|
1023
1048
|
>>> q2.question_name = "q2"
|
1024
1049
|
>>> s = Survey([q0, q1, q2])
|
1025
1050
|
>>> s = s.add_skip_rule("q1", "{{ q0.answer }} == 'skip next'")
|
1026
1051
|
"""
|
1027
1052
|
question_index = self._get_question_index(question)
|
1028
|
-
|
1053
|
+
|
1029
1054
|
# Only proceed if question_index is an integer (not EndOfSurvey)
|
1030
1055
|
if isinstance(question_index, int):
|
1031
1056
|
next_index = question_index + 1
|
@@ -1033,9 +1058,7 @@ class Survey(Base):
|
|
1033
1058
|
question, expression, next_index, before_rule=True
|
1034
1059
|
)
|
1035
1060
|
else:
|
1036
|
-
raise SurveyCreationError(
|
1037
|
-
"Cannot add skip rule to EndOfSurvey"
|
1038
|
-
)
|
1061
|
+
raise SurveyCreationError("Cannot add skip rule to EndOfSurvey")
|
1039
1062
|
|
1040
1063
|
def add_rule(
|
1041
1064
|
self,
|
@@ -1045,11 +1068,11 @@ class Survey(Base):
|
|
1045
1068
|
before_rule: bool = False,
|
1046
1069
|
) -> Survey:
|
1047
1070
|
"""Add a conditional rule for navigating between questions in the survey.
|
1048
|
-
|
1071
|
+
|
1049
1072
|
Rules determine the flow of questions based on conditional expressions. When a rule's
|
1050
1073
|
expression evaluates to True, the survey will navigate to the specified next question,
|
1051
1074
|
potentially skipping questions or jumping to an earlier question.
|
1052
|
-
|
1075
|
+
|
1053
1076
|
By default, rules are evaluated *after* a question is answered. When before_rule=True,
|
1054
1077
|
the rule is evaluated before the question is presented (which is useful for skip logic).
|
1055
1078
|
|
@@ -1064,19 +1087,19 @@ class Survey(Base):
|
|
1064
1087
|
or the EndOfSurvey class to end the survey.
|
1065
1088
|
before_rule: If True, the rule is evaluated before the question is presented.
|
1066
1089
|
If False (default), the rule is evaluated after the question is answered.
|
1067
|
-
|
1090
|
+
|
1068
1091
|
Returns:
|
1069
1092
|
Survey: The modified survey instance (allows for method chaining).
|
1070
1093
|
|
1071
1094
|
Examples:
|
1072
1095
|
Add a rule that navigates to q2 if the answer to q0 is 'yes':
|
1073
|
-
|
1096
|
+
|
1074
1097
|
>>> s = Survey.example().add_rule("q0", "{{ q0.answer }} == 'yes'", "q2")
|
1075
1098
|
>>> s.next_question("q0", {"q0.answer": "yes"}).question_name
|
1076
1099
|
'q2'
|
1077
|
-
|
1100
|
+
|
1078
1101
|
Add a rule to end the survey conditionally:
|
1079
|
-
|
1102
|
+
|
1080
1103
|
>>> from edsl.surveys.base import EndOfSurvey
|
1081
1104
|
>>> s = Survey.example().add_rule("q0", "{{ q0.answer }} == 'end'", EndOfSurvey)
|
1082
1105
|
"""
|
@@ -1086,11 +1109,11 @@ class Survey(Base):
|
|
1086
1109
|
|
1087
1110
|
def by(self, *args: Union["Agent", "Scenario", "LanguageModel"]) -> "Jobs":
|
1088
1111
|
"""Add components to the survey and return a runnable Jobs object.
|
1089
|
-
|
1112
|
+
|
1090
1113
|
This method is the primary way to prepare a survey for execution. It adds the
|
1091
1114
|
necessary components (agents, scenarios, language models) to create a Jobs object
|
1092
1115
|
that can be run to generate responses to the survey.
|
1093
|
-
|
1116
|
+
|
1094
1117
|
The method can be chained to add multiple components in sequence.
|
1095
1118
|
|
1096
1119
|
Args:
|
@@ -1098,21 +1121,21 @@ class Survey(Base):
|
|
1098
1121
|
- Agent: The persona that will answer the survey questions
|
1099
1122
|
- Scenario: The context for the survey, with variables to substitute
|
1100
1123
|
- LanguageModel: The model that will generate the agent's responses
|
1101
|
-
|
1124
|
+
|
1102
1125
|
Returns:
|
1103
1126
|
Jobs: A Jobs object that can be run to execute the survey.
|
1104
|
-
|
1127
|
+
|
1105
1128
|
Examples:
|
1106
1129
|
Create a runnable Jobs object with an agent and scenario:
|
1107
|
-
|
1130
|
+
|
1108
1131
|
>>> s = Survey.example()
|
1109
1132
|
>>> from edsl.agents import Agent
|
1110
1133
|
>>> from edsl import Scenario
|
1111
1134
|
>>> s.by(Agent.example()).by(Scenario.example())
|
1112
1135
|
Jobs(...)
|
1113
|
-
|
1136
|
+
|
1114
1137
|
Chain all components in a single call:
|
1115
|
-
|
1138
|
+
|
1116
1139
|
>>> from edsl.language_models import LanguageModel
|
1117
1140
|
>>> s.by(Agent.example(), Scenario.example(), LanguageModel.example())
|
1118
1141
|
Jobs(...)
|
@@ -1123,14 +1146,14 @@ class Survey(Base):
|
|
1123
1146
|
|
1124
1147
|
def to_jobs(self) -> "Jobs":
|
1125
1148
|
"""Convert the survey to a Jobs object without adding components.
|
1126
|
-
|
1149
|
+
|
1127
1150
|
This method creates a Jobs object from the survey without adding any agents,
|
1128
1151
|
scenarios, or language models. You'll need to add these components later
|
1129
1152
|
using the `by()` method before running the job.
|
1130
|
-
|
1153
|
+
|
1131
1154
|
Returns:
|
1132
1155
|
Jobs: A Jobs object based on this survey.
|
1133
|
-
|
1156
|
+
|
1134
1157
|
Examples:
|
1135
1158
|
>>> s = Survey.example()
|
1136
1159
|
>>> jobs = s.to_jobs()
|
@@ -1143,11 +1166,11 @@ class Survey(Base):
|
|
1143
1166
|
|
1144
1167
|
def show_prompts(self):
|
1145
1168
|
"""Display the prompts that will be used when running the survey.
|
1146
|
-
|
1169
|
+
|
1147
1170
|
This method converts the survey to a Jobs object and shows the prompts that
|
1148
1171
|
would be sent to a language model. This is useful for debugging and understanding
|
1149
1172
|
how the survey will be presented.
|
1150
|
-
|
1173
|
+
|
1151
1174
|
Returns:
|
1152
1175
|
The detailed prompts for the survey.
|
1153
1176
|
"""
|
@@ -1164,10 +1187,10 @@ class Survey(Base):
|
|
1164
1187
|
**kwargs,
|
1165
1188
|
) -> "Results":
|
1166
1189
|
"""Execute the survey with the given parameters and return results.
|
1167
|
-
|
1190
|
+
|
1168
1191
|
This is a convenient shorthand for creating a Jobs object and running it immediately.
|
1169
1192
|
Any keyword arguments are passed as scenario parameters.
|
1170
|
-
|
1193
|
+
|
1171
1194
|
Args:
|
1172
1195
|
model: The language model to use. If None, a default model is used.
|
1173
1196
|
agent: The agent to use. If None, a default agent is used.
|
@@ -1176,13 +1199,13 @@ class Survey(Base):
|
|
1176
1199
|
disable_remote_cache: If True, don't use remote cache even if available.
|
1177
1200
|
disable_remote_inference: If True, don't use remote inference even if available.
|
1178
1201
|
**kwargs: Key-value pairs to use as scenario parameters.
|
1179
|
-
|
1202
|
+
|
1180
1203
|
Returns:
|
1181
1204
|
Results: The results of running the survey.
|
1182
|
-
|
1205
|
+
|
1183
1206
|
Examples:
|
1184
1207
|
Run a survey with a functional question that uses scenario parameters:
|
1185
|
-
|
1208
|
+
|
1186
1209
|
>>> from edsl.questions import QuestionFunctional
|
1187
1210
|
>>> def f(scenario, agent_traits): return "yes" if scenario["period"] == "morning" else "no"
|
1188
1211
|
>>> q = QuestionFunctional(question_name="q0", func=f)
|
@@ -1207,11 +1230,11 @@ class Survey(Base):
|
|
1207
1230
|
**kwargs,
|
1208
1231
|
) -> "Results":
|
1209
1232
|
"""Execute the survey asynchronously and return results.
|
1210
|
-
|
1233
|
+
|
1211
1234
|
This method provides an asynchronous way to run surveys, which is useful for
|
1212
1235
|
concurrent execution or integration with other async code. It creates a Jobs
|
1213
1236
|
object and runs it asynchronously.
|
1214
|
-
|
1237
|
+
|
1215
1238
|
Args:
|
1216
1239
|
model: The language model to use. If None, a default model is used.
|
1217
1240
|
agent: The agent to use. If None, a default agent is used.
|
@@ -1219,28 +1242,28 @@ class Survey(Base):
|
|
1219
1242
|
**kwargs: Key-value pairs to use as scenario parameters. May include:
|
1220
1243
|
- disable_remote_inference: If True, don't use remote inference even if available.
|
1221
1244
|
- disable_remote_cache: If True, don't use remote cache even if available.
|
1222
|
-
|
1245
|
+
|
1223
1246
|
Returns:
|
1224
1247
|
Results: The results of running the survey.
|
1225
|
-
|
1248
|
+
|
1226
1249
|
Examples:
|
1227
1250
|
Run a survey asynchronously with morning parameter:
|
1228
|
-
|
1251
|
+
|
1229
1252
|
>>> import asyncio
|
1230
1253
|
>>> from edsl.questions import QuestionFunctional
|
1231
1254
|
>>> def f(scenario, agent_traits): return "yes" if scenario["period"] == "morning" else "no"
|
1232
1255
|
>>> q = QuestionFunctional(question_name="q0", func=f)
|
1233
|
-
>>> from edsl import Model
|
1256
|
+
>>> from edsl import Model
|
1234
1257
|
>>> s = Survey([q])
|
1235
|
-
>>> async def test_run_async():
|
1258
|
+
>>> async def test_run_async():
|
1236
1259
|
... result = await s.run_async(period="morning", disable_remote_inference = True)
|
1237
1260
|
... print(result.select("answer.q0").first())
|
1238
1261
|
>>> asyncio.run(test_run_async())
|
1239
1262
|
yes
|
1240
|
-
|
1263
|
+
|
1241
1264
|
Run with evening parameter:
|
1242
|
-
|
1243
|
-
>>> async def test_run_async2():
|
1265
|
+
|
1266
|
+
>>> async def test_run_async2():
|
1244
1267
|
... result = await s.run_async(period="evening", disable_remote_inference = True)
|
1245
1268
|
... print(result.select("answer.q0").first())
|
1246
1269
|
>>> asyncio.run(test_run_async2())
|
@@ -1249,28 +1272,37 @@ class Survey(Base):
|
|
1249
1272
|
# Create a cache if none provided
|
1250
1273
|
if cache is None:
|
1251
1274
|
from edsl.caching import Cache
|
1275
|
+
|
1252
1276
|
c = Cache()
|
1253
1277
|
else:
|
1254
1278
|
c = cache
|
1255
1279
|
|
1256
1280
|
# Get scenario parameters, excluding any that will be passed to run_async
|
1257
|
-
scenario_kwargs = {
|
1258
|
-
|
1259
|
-
|
1281
|
+
scenario_kwargs = {
|
1282
|
+
k: v
|
1283
|
+
for k, v in kwargs.items()
|
1284
|
+
if k not in ["disable_remote_inference", "disable_remote_cache"]
|
1285
|
+
}
|
1286
|
+
|
1260
1287
|
# Get the job options to pass to run_async
|
1261
|
-
job_kwargs = {
|
1262
|
-
|
1263
|
-
|
1264
|
-
|
1288
|
+
job_kwargs = {
|
1289
|
+
k: v
|
1290
|
+
for k, v in kwargs.items()
|
1291
|
+
if k in ["disable_remote_inference", "disable_remote_cache"]
|
1292
|
+
}
|
1293
|
+
|
1294
|
+
jobs: "Jobs" = self.get_job(model=model, agent=agent, **scenario_kwargs).using(
|
1295
|
+
c
|
1296
|
+
)
|
1265
1297
|
return await jobs.run_async(**job_kwargs)
|
1266
1298
|
|
1267
1299
|
def run(self, *args, **kwargs) -> "Results":
|
1268
1300
|
"""Convert the survey to a Job and execute it with the provided parameters.
|
1269
|
-
|
1301
|
+
|
1270
1302
|
This method creates a Jobs object from the survey and runs it immediately with
|
1271
1303
|
the provided arguments. It's a convenient way to run a survey without explicitly
|
1272
1304
|
creating a Jobs object first.
|
1273
|
-
|
1305
|
+
|
1274
1306
|
Args:
|
1275
1307
|
*args: Positional arguments passed to the Jobs.run() method.
|
1276
1308
|
**kwargs: Keyword arguments passed to the Jobs.run() method, which can include:
|
@@ -1278,13 +1310,13 @@ class Survey(Base):
|
|
1278
1310
|
- verbose: Whether to show detailed progress
|
1279
1311
|
- disable_remote_cache: Whether to disable remote caching
|
1280
1312
|
- disable_remote_inference: Whether to disable remote inference
|
1281
|
-
|
1313
|
+
|
1282
1314
|
Returns:
|
1283
1315
|
Results: The results of running the survey.
|
1284
|
-
|
1316
|
+
|
1285
1317
|
Examples:
|
1286
1318
|
Run a survey with a test language model:
|
1287
|
-
|
1319
|
+
|
1288
1320
|
>>> from edsl import QuestionFreeText
|
1289
1321
|
>>> s = Survey([QuestionFreeText.example()])
|
1290
1322
|
>>> from edsl.language_models import LanguageModel
|
@@ -1364,12 +1396,12 @@ class Survey(Base):
|
|
1364
1396
|
|
1365
1397
|
def gen_path_through_survey(self) -> Generator[QuestionBase, dict, None]:
|
1366
1398
|
"""Generate a coroutine that navigates through the survey based on answers.
|
1367
|
-
|
1399
|
+
|
1368
1400
|
This method creates a Python generator that implements the survey flow logic.
|
1369
1401
|
It yields questions and receives answers, handling the branching logic based
|
1370
1402
|
on the rules defined in the survey. This generator is the core mechanism used
|
1371
1403
|
by the Interview process to administer surveys.
|
1372
|
-
|
1404
|
+
|
1373
1405
|
The generator follows these steps:
|
1374
1406
|
1. Yields the first question (or skips it if skip rules apply)
|
1375
1407
|
2. Receives an answer dictionary from the caller via .send()
|
@@ -1377,29 +1409,29 @@ class Survey(Base):
|
|
1377
1409
|
4. Determines the next question based on the survey rules
|
1378
1410
|
5. Yields the next question
|
1379
1411
|
6. Repeats steps 2-5 until the end of survey is reached
|
1380
|
-
|
1412
|
+
|
1381
1413
|
Returns:
|
1382
1414
|
Generator[QuestionBase, dict, None]: A generator that yields questions and
|
1383
1415
|
receives answer dictionaries. The generator terminates when it reaches
|
1384
1416
|
the end of the survey.
|
1385
|
-
|
1417
|
+
|
1386
1418
|
Examples:
|
1387
1419
|
For the example survey with conditional branching:
|
1388
|
-
|
1420
|
+
|
1389
1421
|
>>> s = Survey.example()
|
1390
1422
|
>>> s.show_rules()
|
1391
1423
|
Dataset([{'current_q': [0, 0, 1, 2]}, {'expression': ['True', "{{ q0.answer }}== 'yes'", 'True', 'True']}, {'next_q': [1, 2, 2, 3]}, {'priority': [-1, 0, -1, -1]}, {'before_rule': [False, False, False, False]}])
|
1392
|
-
|
1424
|
+
|
1393
1425
|
Path when answering "yes" to first question:
|
1394
|
-
|
1426
|
+
|
1395
1427
|
>>> i = s.gen_path_through_survey()
|
1396
1428
|
>>> next(i) # Get first question
|
1397
1429
|
Question('multiple_choice', question_name = \"""q0\""", question_text = \"""Do you like school?\""", question_options = ['yes', 'no'])
|
1398
1430
|
>>> i.send({"q0.answer": "yes"}) # Answer "yes" and get next question
|
1399
1431
|
Question('multiple_choice', question_name = \"""q2\""", question_text = \"""Why?\""", question_options = ['**lack*** of killer bees in cafeteria', 'other'])
|
1400
|
-
|
1432
|
+
|
1401
1433
|
Path when answering "no" to first question:
|
1402
|
-
|
1434
|
+
|
1403
1435
|
>>> i2 = s.gen_path_through_survey()
|
1404
1436
|
>>> next(i2) # Get first question
|
1405
1437
|
Question('multiple_choice', question_name = \"""q0\""", question_text = \"""Do you like school?\""", question_options = ['yes', 'no'])
|
@@ -1408,10 +1440,10 @@ class Survey(Base):
|
|
1408
1440
|
"""
|
1409
1441
|
# Initialize empty answers dictionary
|
1410
1442
|
self.answers: Dict[str, Any] = {}
|
1411
|
-
|
1443
|
+
|
1412
1444
|
# Start with the first question
|
1413
1445
|
question = self.questions[0]
|
1414
|
-
|
1446
|
+
|
1415
1447
|
# Check if the first question should be skipped based on skip rules
|
1416
1448
|
if self.rule_collection.skip_question_before_running(0, self.answers):
|
1417
1449
|
question = self.next_question(question, self.answers)
|
@@ -1420,39 +1452,38 @@ class Survey(Base):
|
|
1420
1452
|
while not question == EndOfSurvey:
|
1421
1453
|
# Yield the current question and wait for an answer
|
1422
1454
|
answer = yield question
|
1423
|
-
|
1455
|
+
|
1424
1456
|
# Update the accumulated answers with the new answer
|
1425
1457
|
self.answers.update(answer)
|
1426
|
-
|
1458
|
+
|
1427
1459
|
# Determine the next question based on the rules and answers
|
1428
1460
|
# TODO: This should also include survey and agent attributes
|
1429
1461
|
question = self.next_question(question, self.answers)
|
1430
1462
|
|
1431
|
-
|
1432
1463
|
def dag(self, textify: bool = False) -> "DAG":
|
1433
1464
|
"""Return a Directed Acyclic Graph (DAG) representation of the survey flow.
|
1434
|
-
|
1465
|
+
|
1435
1466
|
This method constructs a DAG that represents the possible paths through the survey,
|
1436
1467
|
taking into account both skip logic and memory relationships. The DAG is useful
|
1437
1468
|
for visualizing and analyzing the structure of the survey.
|
1438
|
-
|
1469
|
+
|
1439
1470
|
Args:
|
1440
1471
|
textify: If True, the DAG will use question names as nodes instead of indices.
|
1441
1472
|
This makes the DAG more human-readable but less compact.
|
1442
|
-
|
1473
|
+
|
1443
1474
|
Returns:
|
1444
1475
|
DAG: A dictionary where keys are question indices (or names if textify=True)
|
1445
1476
|
and values are sets of prerequisite questions. For example, {2: {0, 1}}
|
1446
1477
|
means question 2 depends on questions 0 and 1.
|
1447
|
-
|
1478
|
+
|
1448
1479
|
Examples:
|
1449
1480
|
>>> s = Survey.example()
|
1450
1481
|
>>> d = s.dag()
|
1451
1482
|
>>> d
|
1452
1483
|
{1: {0}, 2: {0}}
|
1453
|
-
|
1484
|
+
|
1454
1485
|
With textify=True:
|
1455
|
-
|
1486
|
+
|
1456
1487
|
>>> dag = s.dag(textify=True)
|
1457
1488
|
>>> sorted([(k, sorted(list(v))) for k, v in dag.items()])
|
1458
1489
|
[('q1', ['q0']), ('q2', ['q0'])]
|
@@ -1529,12 +1560,12 @@ class Survey(Base):
|
|
1529
1560
|
custom_instructions: Optional[str] = None,
|
1530
1561
|
) -> Survey:
|
1531
1562
|
"""Create an example survey for testing and demonstration purposes.
|
1532
|
-
|
1563
|
+
|
1533
1564
|
This method creates a simple branching survey about school preferences.
|
1534
1565
|
The default survey contains three questions with conditional logic:
|
1535
1566
|
- If the user answers "yes" to liking school, they are asked why they like it
|
1536
1567
|
- If the user answers "no", they are asked why they don't like it
|
1537
|
-
|
1568
|
+
|
1538
1569
|
Args:
|
1539
1570
|
params: If True, adds a fourth question that demonstrates parameter substitution
|
1540
1571
|
by referencing the question text and answer from the first question.
|
@@ -1543,19 +1574,19 @@ class Survey(Base):
|
|
1543
1574
|
include_instructions: If True, adds an instruction to the beginning of the survey.
|
1544
1575
|
custom_instructions: Custom instruction text to use if include_instructions is True.
|
1545
1576
|
Defaults to "Please pay attention!" if not provided.
|
1546
|
-
|
1577
|
+
|
1547
1578
|
Returns:
|
1548
1579
|
Survey: A configured example survey instance.
|
1549
|
-
|
1580
|
+
|
1550
1581
|
Examples:
|
1551
1582
|
Create a basic example survey:
|
1552
|
-
|
1583
|
+
|
1553
1584
|
>>> s = Survey.example()
|
1554
1585
|
>>> [q.question_text for q in s.questions]
|
1555
1586
|
['Do you like school?', 'Why not?', 'Why?']
|
1556
|
-
|
1587
|
+
|
1557
1588
|
Create an example with parameter substitution:
|
1558
|
-
|
1589
|
+
|
1559
1590
|
>>> s = Survey.example(params=True)
|
1560
1591
|
>>> s.questions[3].question_text
|
1561
1592
|
"To the question '{{ q0.question_text}}', you said '{{ q0.answer }}'. Do you still feel this way?"
|
@@ -1564,7 +1595,7 @@ class Survey(Base):
|
|
1564
1595
|
|
1565
1596
|
# Add random UUID to question text if randomization is requested
|
1566
1597
|
addition = "" if not randomize else str(uuid4())
|
1567
|
-
|
1598
|
+
|
1568
1599
|
# Create the basic questions
|
1569
1600
|
q0 = QuestionMultipleChoice(
|
1570
1601
|
question_text=f"Do you like school?{addition}",
|
@@ -1581,7 +1612,7 @@ class Survey(Base):
|
|
1581
1612
|
question_options=["**lack*** of killer bees in cafeteria", "other"],
|
1582
1613
|
question_name="q2",
|
1583
1614
|
)
|
1584
|
-
|
1615
|
+
|
1585
1616
|
# Add parameter demonstration question if requested
|
1586
1617
|
if params:
|
1587
1618
|
q3 = QuestionMultipleChoice(
|
@@ -1645,7 +1676,11 @@ class Survey(Base):
|
|
1645
1676
|
|
1646
1677
|
c = Coop()
|
1647
1678
|
project_details = c.create_project(
|
1648
|
-
self,
|
1679
|
+
self,
|
1680
|
+
project_name=project_name,
|
1681
|
+
survey_description=survey_description,
|
1682
|
+
survey_alias=survey_alias,
|
1683
|
+
survey_visibility=survey_visibility,
|
1649
1684
|
)
|
1650
1685
|
return project_details
|
1651
1686
|
|
@@ -1697,14 +1732,14 @@ class Survey(Base):
|
|
1697
1732
|
|
1698
1733
|
def copy(self) -> "Survey":
|
1699
1734
|
"""Create a deep copy of the survey using serialization.
|
1700
|
-
|
1735
|
+
|
1701
1736
|
This method creates a completely independent copy of the survey by serializing
|
1702
1737
|
and then deserializing it. This ensures all components are properly copied
|
1703
1738
|
and maintains consistency with the survey's serialization format.
|
1704
|
-
|
1739
|
+
|
1705
1740
|
Returns:
|
1706
1741
|
Survey: A new Survey instance that is a deep copy of the original.
|
1707
|
-
|
1742
|
+
|
1708
1743
|
Examples:
|
1709
1744
|
>>> s = Survey.example()
|
1710
1745
|
>>> s2 = s.copy()
|