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/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 hasattr(result, 'true_questions') and hasattr(result, 'instruction_names_to_instructions') and hasattr(result, 'pseudo_indices'):
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(f"Unexpected result type from separate_questions_and_instructions: {type(result)}")
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, (exist_start, exist_end) in self.question_groups.items():
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 = {k: v for k, v in kwargs.items()
1258
- if k not in ['disable_remote_inference', 'disable_remote_cache']}
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 = {k: v for k, v in kwargs.items()
1262
- if k in ['disable_remote_inference', 'disable_remote_cache']}
1263
-
1264
- jobs: "Jobs" = self.get_job(model=model, agent=agent, **scenario_kwargs).using(c)
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, project_name, survey_description, survey_alias, survey_visibility
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()