edsl 0.1.50__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.
Files changed (109) hide show
  1. edsl/__version__.py +1 -1
  2. edsl/base/base_exception.py +2 -2
  3. edsl/buckets/bucket_collection.py +1 -1
  4. edsl/buckets/exceptions.py +32 -0
  5. edsl/buckets/token_bucket_api.py +26 -10
  6. edsl/caching/cache.py +5 -2
  7. edsl/caching/remote_cache_sync.py +5 -5
  8. edsl/caching/sql_dict.py +12 -11
  9. edsl/config/__init__.py +1 -1
  10. edsl/config/config_class.py +4 -2
  11. edsl/conversation/Conversation.py +7 -4
  12. edsl/conversation/car_buying.py +1 -3
  13. edsl/conversation/mug_negotiation.py +2 -6
  14. edsl/coop/__init__.py +11 -8
  15. edsl/coop/coop.py +13 -13
  16. edsl/coop/coop_functions.py +1 -1
  17. edsl/coop/ep_key_handling.py +1 -1
  18. edsl/coop/price_fetcher.py +2 -2
  19. edsl/coop/utils.py +2 -2
  20. edsl/dataset/dataset.py +144 -63
  21. edsl/dataset/dataset_operations_mixin.py +14 -6
  22. edsl/dataset/dataset_tree.py +3 -3
  23. edsl/dataset/display/table_renderers.py +6 -3
  24. edsl/dataset/file_exports.py +4 -4
  25. edsl/dataset/r/ggplot.py +3 -3
  26. edsl/inference_services/available_model_fetcher.py +2 -2
  27. edsl/inference_services/data_structures.py +5 -5
  28. edsl/inference_services/inference_service_abc.py +1 -1
  29. edsl/inference_services/inference_services_collection.py +1 -1
  30. edsl/inference_services/service_availability.py +3 -3
  31. edsl/inference_services/services/azure_ai.py +3 -3
  32. edsl/inference_services/services/google_service.py +1 -1
  33. edsl/inference_services/services/test_service.py +1 -1
  34. edsl/instructions/change_instruction.py +5 -4
  35. edsl/instructions/instruction.py +1 -0
  36. edsl/instructions/instruction_collection.py +5 -4
  37. edsl/instructions/instruction_handler.py +10 -8
  38. edsl/interviews/exception_tracking.py +1 -1
  39. edsl/interviews/interview.py +1 -1
  40. edsl/interviews/interview_status_dictionary.py +1 -1
  41. edsl/interviews/interview_task_manager.py +2 -2
  42. edsl/interviews/request_token_estimator.py +3 -2
  43. edsl/interviews/statistics.py +2 -2
  44. edsl/invigilators/invigilators.py +2 -2
  45. edsl/jobs/__init__.py +39 -2
  46. edsl/jobs/async_interview_runner.py +1 -1
  47. edsl/jobs/check_survey_scenario_compatibility.py +5 -5
  48. edsl/jobs/data_structures.py +2 -2
  49. edsl/jobs/jobs.py +2 -2
  50. edsl/jobs/jobs_checks.py +5 -5
  51. edsl/jobs/jobs_component_constructor.py +2 -2
  52. edsl/jobs/jobs_pricing_estimation.py +1 -1
  53. edsl/jobs/jobs_runner_asyncio.py +2 -2
  54. edsl/jobs/remote_inference.py +1 -1
  55. edsl/jobs/results_exceptions_handler.py +2 -2
  56. edsl/language_models/language_model.py +5 -1
  57. edsl/notebooks/__init__.py +24 -1
  58. edsl/notebooks/exceptions.py +82 -0
  59. edsl/notebooks/notebook.py +7 -3
  60. edsl/notebooks/notebook_to_latex.py +1 -1
  61. edsl/prompts/__init__.py +23 -2
  62. edsl/prompts/prompt.py +1 -1
  63. edsl/questions/__init__.py +4 -4
  64. edsl/questions/answer_validator_mixin.py +0 -5
  65. edsl/questions/compose_questions.py +2 -2
  66. edsl/questions/descriptors.py +1 -1
  67. edsl/questions/question_base.py +32 -3
  68. edsl/questions/question_base_prompts_mixin.py +4 -4
  69. edsl/questions/question_budget.py +503 -102
  70. edsl/questions/question_check_box.py +658 -156
  71. edsl/questions/question_dict.py +176 -2
  72. edsl/questions/question_extract.py +401 -61
  73. edsl/questions/question_free_text.py +77 -9
  74. edsl/questions/question_functional.py +118 -9
  75. edsl/questions/{derived/question_likert_five.py → question_likert_five.py} +2 -2
  76. edsl/questions/{derived/question_linear_scale.py → question_linear_scale.py} +3 -4
  77. edsl/questions/question_list.py +246 -26
  78. edsl/questions/question_matrix.py +586 -73
  79. edsl/questions/question_multiple_choice.py +213 -47
  80. edsl/questions/question_numerical.py +360 -29
  81. edsl/questions/question_rank.py +401 -124
  82. edsl/questions/question_registry.py +3 -3
  83. edsl/questions/{derived/question_top_k.py → question_top_k.py} +3 -3
  84. edsl/questions/{derived/question_yes_no.py → question_yes_no.py} +3 -4
  85. edsl/questions/register_questions_meta.py +2 -1
  86. edsl/questions/response_validator_abc.py +6 -2
  87. edsl/questions/response_validator_factory.py +10 -12
  88. edsl/results/report.py +1 -1
  89. edsl/results/result.py +7 -4
  90. edsl/results/results.py +471 -271
  91. edsl/results/results_selector.py +2 -2
  92. edsl/scenarios/construct_download_link.py +3 -3
  93. edsl/scenarios/scenario.py +1 -2
  94. edsl/scenarios/scenario_list.py +41 -23
  95. edsl/surveys/survey_css.py +3 -3
  96. edsl/surveys/survey_simulator.py +2 -1
  97. edsl/tasks/__init__.py +22 -2
  98. edsl/tasks/exceptions.py +72 -0
  99. edsl/tasks/task_history.py +3 -3
  100. edsl/tokens/__init__.py +27 -1
  101. edsl/tokens/exceptions.py +37 -0
  102. edsl/tokens/interview_token_usage.py +3 -2
  103. edsl/tokens/token_usage.py +4 -3
  104. {edsl-0.1.50.dist-info → edsl-0.1.51.dist-info}/METADATA +1 -1
  105. {edsl-0.1.50.dist-info → edsl-0.1.51.dist-info}/RECORD +108 -106
  106. edsl/questions/derived/__init__.py +0 -0
  107. {edsl-0.1.50.dist-info → edsl-0.1.51.dist-info}/LICENSE +0 -0
  108. {edsl-0.1.50.dist-info → edsl-0.1.51.dist-info}/WHEEL +0 -0
  109. {edsl-0.1.50.dist-info → edsl-0.1.51.dist-info}/entry_points.txt +0 -0
@@ -9,6 +9,29 @@ from .descriptors import QuestionOptionsDescriptor
9
9
  from .decorators import inject_exception
10
10
  from .response_validator_abc import ResponseValidatorABC
11
11
 
12
+ class BaseMultipleChoiceResponse(BaseModel):
13
+ """
14
+ Base model for multiple choice responses.
15
+
16
+ Attributes:
17
+ answer: The selected choice
18
+ comment: Optional comment field
19
+ generated_tokens: Optional raw tokens generated by the model
20
+ """
21
+ answer: Any = Field(..., description="Selected choice")
22
+ comment: Optional[str] = Field(None, description="Optional comment field")
23
+ generated_tokens: Optional[Any] = Field(None, description="Generated tokens")
24
+
25
+ """
26
+ Examples:
27
+ >>> model = BaseMultipleChoiceResponse(answer="Option A", comment="My reasoning")
28
+ >>> model.answer
29
+ 'Option A'
30
+ >>> model.comment
31
+ 'My reasoning'
32
+ """
33
+
34
+
12
35
  def create_response_model(choices: List[str], permissive: bool = False):
13
36
  """
14
37
  Create a ChoiceResponse model class with a predefined list of choices.
@@ -16,95 +39,238 @@ def create_response_model(choices: List[str], permissive: bool = False):
16
39
  :param choices: A list of allowed values for the answer field.
17
40
  :param permissive: If True, any value will be accepted as an answer.
18
41
  :return: A new Pydantic model class.
42
+
43
+ Examples:
44
+ >>> model = create_response_model(["Red", "Green", "Blue"], permissive=False)
45
+ >>> response = model(answer="Red")
46
+ >>> response.answer
47
+ 'Red'
48
+
49
+ >>> try:
50
+ ... model(answer="Purple")
51
+ ... except Exception:
52
+ ... print("Invalid value")
53
+ Invalid value
54
+
55
+ >>> permissive_model = create_response_model(["Red", "Green", "Blue"], permissive=True)
56
+ >>> response = permissive_model(answer="Purple")
57
+ >>> response.answer
58
+ 'Purple'
19
59
  """
20
60
  choice_tuple = tuple(choices)
21
61
 
22
62
  if not permissive:
23
-
24
- class ChoiceResponse(BaseModel):
63
+ class ChoiceResponse(BaseMultipleChoiceResponse):
64
+ """
65
+ A model for multiple choice responses with strict validation.
66
+
67
+ Attributes:
68
+ answer: Must be one of the predefined choices
69
+
70
+ Examples:
71
+ >>> choices = ["Option A", "Option B", "Option C"]
72
+ >>> model = create_response_model(choices, permissive=False)
73
+ >>> response = model(answer="Option A")
74
+ >>> response.answer
75
+ 'Option A'
76
+ """
25
77
  answer: Literal[choice_tuple] = Field(description="Selected choice")
26
- comment: Optional[str] = Field(None, description="Optional comment field")
27
- generated_tokens: Optional[Any] = Field(
28
- None, description="Generated tokens"
29
- )
30
-
31
- class Config:
32
- @staticmethod
33
- def json_schema_extra(schema: dict, model: BaseModel) -> None:
34
- for prop in schema.get("properties", {}).values():
35
- if prop.get("title") == "answer":
36
- prop["enum"] = choices
37
78
 
79
+ model_config = {
80
+ "json_schema_extra": {
81
+ "properties": {
82
+ "answer": {
83
+ "enum": choices
84
+ }
85
+ }
86
+ }
87
+ }
38
88
  else:
89
+ class ChoiceResponse(BaseMultipleChoiceResponse):
90
+ """
91
+ A model for multiple choice responses with permissive validation.
92
+
93
+ Attributes:
94
+ answer: Can be any value, with suggested choices provided
95
+
96
+ Examples:
97
+ >>> choices = ["Option A", "Option B", "Option C"]
98
+ >>> model = create_response_model(choices, permissive=True)
99
+ >>> response = model(answer="Something Else")
100
+ >>> response.answer
101
+ 'Something Else'
102
+ """
103
+ answer: Any = Field(description=f"Selected choice (can be any value). Suggested choices are: {choices}")
39
104
 
40
- class ChoiceResponse(BaseModel):
41
- answer: Any = Field(description="Selected choice (can be any value)")
42
- comment: Optional[str] = Field(None, description="Optional comment field")
43
- generated_tokens: Optional[Any] = Field(
44
- None, description="Generated tokens"
45
- )
46
-
47
- class Config:
48
- @staticmethod
49
- def json_schema_extra(schema: dict, model: BaseModel) -> None:
50
- for prop in schema.get("properties", {}).values():
51
- if prop.get("title") == "answer":
52
- prop["description"] += f". Suggested choices are: {choices}"
53
- schema["title"] += " (Permissive)"
105
+ model_config = {
106
+ "title": "PermissiveChoiceResponse"
107
+ }
54
108
 
55
109
  return ChoiceResponse
56
110
 
57
111
 
58
112
  class MultipleChoiceResponseValidator(ResponseValidatorABC):
113
+ """
114
+ Validator for multiple choice responses.
115
+
116
+ This validator ensures that the answer is one of the allowed options.
117
+ In permissive mode, any answer is accepted.
118
+
119
+ Examples:
120
+ >>> from edsl.questions import QuestionMultipleChoice
121
+ >>> q = QuestionMultipleChoice(
122
+ ... question_name="feeling",
123
+ ... question_text="How are you feeling?",
124
+ ... question_options=["Good", "Bad", "Neutral"]
125
+ ... )
126
+ >>> validator = q.response_validator
127
+ >>> result = validator.validate({"answer": "Good"})
128
+ >>> sorted(result.keys())
129
+ ['answer', 'comment', 'generated_tokens']
130
+ >>> result["answer"]
131
+ 'Good'
132
+ """
59
133
  required_params = ["question_options", "use_code"]
60
134
 
61
135
  def fix(self, response, verbose=False):
62
- response_text = str(response.get("answer"))
63
- if response_text is None:
64
- response_text = response.get("generated_tokens", "")
136
+ """
137
+ Attempt to fix an invalid multiple choice response.
138
+
139
+ Strategies:
140
+ 1. Extract an option mentioned in the generated text
141
+ 2. Check for exact matches in the text
142
+ 3. Look for substring matches
143
+ 4. Normalize whitespace and check for matches
144
+ 5. Check if the answer is a prefix of any option (ignoring trailing spaces/punctuation)
145
+
146
+ Parameters:
147
+ response: The invalid response to fix
148
+ verbose: Whether to print debug information
149
+
150
+ Returns:
151
+ A fixed response dict if possible, otherwise the original response
152
+
153
+ Examples:
154
+ >>> from edsl.questions import QuestionMultipleChoice
155
+ >>> q = QuestionMultipleChoice.example()
156
+ >>> validator = q.response_validator
157
+ >>> result = validator.fix({"answer": "I'm feeling Good today"})
158
+ >>> sorted(result.keys())
159
+ ['answer', 'comment', 'generated_tokens']
160
+ >>> result["answer"]
161
+ 'Good'
162
+ """
163
+ # Don't attempt to fix None values - they should be properly rejected
164
+ if response.get("answer") is None:
165
+ if verbose:
166
+ print("Not attempting to fix None answer value")
167
+ return response
168
+
169
+ # Get the raw text to analyze
170
+ response_text = str(response.get("answer", ""))
171
+ if not response_text:
172
+ response_text = str(response.get("generated_tokens", ""))
65
173
 
66
174
  if verbose:
67
- print(f"Invalid generated tokens was: {response_text}")
175
+ print(f"Invalid response text: {response_text}")
176
+ print(f"Looking for options among: {self.question_options}")
68
177
 
178
+ # Strategy 1: Look for exact options in the text
69
179
  matches = []
70
- for idx, option in enumerate(self.question_options):
71
- if verbose:
72
- print("The options are: ", self.question_options)
73
- if str(option) in response_text:
180
+ for option in self.question_options:
181
+ option_str = str(option)
182
+ if option_str in response_text:
74
183
  if verbose:
75
- print("Match found with option ", option)
184
+ print(f"Match found with option: {option_str}")
76
185
  if option not in matches:
77
186
  matches.append(option)
78
187
 
79
- if verbose:
80
- print("The matches are: ", matches)
188
+ # If we have exactly one match, use it
81
189
  if len(matches) == 1:
190
+ fixed_answer = matches[0]
82
191
  proposed_data = {
83
- "answer": matches[0],
84
- "generated_tokens": response.get("generated_tokens", None),
192
+ "answer": fixed_answer,
193
+ "comment": response.get("comment"),
194
+ "generated_tokens": response.get("generated_tokens"),
85
195
  }
196
+
86
197
  try:
87
- self.response_model(**proposed_data)
198
+ # Validate the fixed answer
199
+ self.response_model.model_validate(proposed_data)
200
+ if verbose:
201
+ print(f"Fixed answer: {fixed_answer}")
88
202
  return proposed_data
89
203
  except Exception as e:
90
204
  if verbose:
91
- print(f"Proposed solution {proposed_data} is invalid. Error: {e}")
92
- return response
205
+ print(f"Validation failed for fixed answer: {e}")
206
+
207
+ # Strategy 2: Check if the answer is a match when normalized (strip whitespace)
208
+ response_text_normalized = response_text.strip()
209
+ for option in self.question_options:
210
+ option_str = str(option).strip()
211
+ if option_str == response_text_normalized:
212
+ if verbose:
213
+ print(f"Normalized match found with option: {option}")
214
+ proposed_data = {
215
+ "answer": option, # Use the exact option from the list
216
+ "comment": response.get("comment"),
217
+ "generated_tokens": response.get("generated_tokens"),
218
+ }
219
+ try:
220
+ self.response_model.model_validate(proposed_data)
221
+ if verbose:
222
+ print(f"Fixed answer with normalization: {option}")
223
+ return proposed_data
224
+ except Exception as e:
225
+ if verbose:
226
+ print(f"Validation failed for normalized answer: {e}")
227
+
228
+ # Strategy 3: Check if the answer is a prefix of any option
229
+ # This handles cases where the model returns a partial answer
230
+ # Only apply this strategy if we have a meaningful response text
231
+ if response_text_normalized and not response_text_normalized.lower() == "none":
232
+ for option in self.question_options:
233
+ option_str = str(option).strip()
234
+ if option_str.startswith(response_text_normalized) or response_text_normalized.startswith(option_str):
235
+ if verbose:
236
+ print(f"Prefix match found with option: {option}")
237
+ proposed_data = {
238
+ "answer": option, # Use the exact option from the list
239
+ "comment": response.get("comment"),
240
+ "generated_tokens": response.get("generated_tokens"),
241
+ }
242
+ try:
243
+ self.response_model.model_validate(proposed_data)
244
+ if verbose:
245
+ print(f"Fixed answer with prefix matching: {option}")
246
+ return proposed_data
247
+ except Exception as e:
248
+ if verbose:
249
+ print(f"Validation failed for prefix answer: {e}")
250
+
251
+ # If multiple or no matches, return original response
252
+ if verbose:
253
+ if len(matches) > 1:
254
+ print(f"Multiple matches found: {matches}, cannot determine correct option")
255
+ else:
256
+ print("No matches found in response text")
257
+
258
+ return response
93
259
 
94
260
  valid_examples = [
95
- ({"answer": 1}, {"question_options": ["Good", "Great", "OK", "Bad"]})
261
+ ({"answer": "Good"}, {"question_options": ["Good", "Great", "OK", "Bad"]})
96
262
  ]
97
263
 
98
264
  invalid_examples = [
99
265
  (
100
- {"answer": -1},
266
+ {"answer": "Terrible"},
101
267
  {"question_options": ["Good", "Great", "OK", "Bad"]},
102
- "Answer code must be a non-negative integer",
268
+ "Value error, Permitted values are 'Good', 'Great', 'OK', 'Bad'",
103
269
  ),
104
270
  (
105
271
  {"answer": None},
106
272
  {"question_options": ["Good", "Great", "OK", "Bad"]},
107
- "Answer code must not be missing.",
273
+ "Answer must not be null",
108
274
  ),
109
275
  ]
110
276