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
@@ -12,9 +12,11 @@ Failure:
12
12
 
13
13
  from __future__ import annotations
14
14
  from typing import Union, Optional, Dict, List, Any, Type
15
- from pydantic import BaseModel, Field, create_model, ValidationError
15
+ from pydantic import BaseModel, Field, create_model
16
16
  from jinja2 import Environment, FileSystemLoader, TemplateNotFound
17
17
  from pathlib import Path
18
+ import re
19
+ import ast
18
20
 
19
21
  from .question_base import QuestionBase
20
22
  from .descriptors import (
@@ -98,9 +100,181 @@ def create_dict_response(
98
100
 
99
101
 
100
102
  class DictResponseValidator(ResponseValidatorABC):
101
- """Optional placeholder if you still want a validator class around it."""
103
+ """
104
+ Validator for dictionary responses with specific keys and value types.
105
+
106
+ This validator ensures that:
107
+ 1. All required keys are present in the answer
108
+ 2. Each value has the correct type as specified
109
+ 3. Extra keys are forbidden unless permissive=True
110
+
111
+ Examples:
112
+ >>> from edsl.questions import QuestionDict
113
+ >>> q = QuestionDict(
114
+ ... question_name="recipe",
115
+ ... question_text="Describe a recipe",
116
+ ... answer_keys=["name", "ingredients", "steps"],
117
+ ... value_types=["str", "list[str]", "list[str]"]
118
+ ... )
119
+ >>> validator = q.response_validator
120
+ >>> result = validator.validate({
121
+ ... "answer": {
122
+ ... "name": "Pancakes",
123
+ ... "ingredients": ["flour", "milk", "eggs"],
124
+ ... "steps": ["Mix", "Cook", "Serve"]
125
+ ... }
126
+ ... })
127
+ >>> sorted(result.keys())
128
+ ['answer', 'comment', 'generated_tokens']
129
+ """
102
130
  required_params = ["answer_keys", "permissive"]
103
131
 
132
+ def fix(self, response, verbose=False):
133
+ """
134
+ Attempt to fix an invalid dictionary response.
135
+
136
+ Examples:
137
+ >>> # Set up validator with proper response model
138
+ >>> from pydantic import BaseModel, create_model, Field
139
+ >>> from typing import Optional
140
+ >>> # Create a proper response model that matches our expected structure
141
+ >>> AnswerModel = create_model('AnswerModel', name=(str, ...), age=(int, ...))
142
+ >>> ResponseModel = create_model(
143
+ ... 'ResponseModel',
144
+ ... answer=(AnswerModel, ...),
145
+ ... comment=(Optional[str], None),
146
+ ... generated_tokens=(Optional[Any], None)
147
+ ... )
148
+ >>> validator = DictResponseValidator(
149
+ ... response_model=ResponseModel,
150
+ ... answer_keys=["name", "age"],
151
+ ... permissive=False
152
+ ... )
153
+ >>> validator.value_types = ["str", "int"]
154
+
155
+ # Fix dictionary with comment on same line
156
+ >>> response = "{'name': 'john', 'age': 23} Here you go."
157
+ >>> result = validator.fix(response)
158
+ >>> dict(result['answer']) # Convert to dict for consistent output
159
+ {'name': 'john', 'age': 23}
160
+ >>> result['comment']
161
+ 'Here you go.'
162
+
163
+ # Fix type conversion (string to int)
164
+ >>> response = {"answer": {"name": "john", "age": "23"}}
165
+ >>> result = validator.fix(response)
166
+ >>> dict(result['answer']) # Convert to dict for consistent output
167
+ {'name': 'john', 'age': 23}
168
+
169
+ # Fix list from comma-separated string
170
+ >>> AnswerModel2 = create_model('AnswerModel2', name=(str, ...), hobbies=(List[str], ...))
171
+ >>> ResponseModel2 = create_model(
172
+ ... 'ResponseModel2',
173
+ ... answer=(AnswerModel2, ...),
174
+ ... comment=(Optional[str], None),
175
+ ... generated_tokens=(Optional[Any], None)
176
+ ... )
177
+ >>> validator = DictResponseValidator(
178
+ ... response_model=ResponseModel2,
179
+ ... answer_keys=["name", "hobbies"],
180
+ ... permissive=False
181
+ ... )
182
+ >>> validator.value_types = ["str", "list[str]"]
183
+ >>> response = {"answer": {"name": "john", "hobbies": "reading, gaming, coding"}}
184
+ >>> result = validator.fix(response)
185
+ >>> dict(result['answer']) # Convert to dict for consistent output
186
+ {'name': 'john', 'hobbies': ['reading', 'gaming', 'coding']}
187
+
188
+ # Handle invalid input gracefully
189
+ >>> response = "not a dictionary"
190
+ >>> validator.fix(response)
191
+ 'not a dictionary'
192
+ """
193
+ # First try to separate dictionary from trailing comment if they're on the same line
194
+ if isinstance(response, str):
195
+ # Try to find where the dictionary ends and comment begins
196
+ try:
197
+ dict_match = re.match(r'(\{.*?\})(.*)', response.strip())
198
+ if dict_match:
199
+ dict_str, comment = dict_match.groups()
200
+ try:
201
+ answer_dict = ast.literal_eval(dict_str)
202
+ response = {
203
+ "answer": answer_dict,
204
+ "comment": comment.strip() if comment.strip() else None
205
+ }
206
+ except (ValueError, SyntaxError):
207
+ pass
208
+ except Exception:
209
+ pass
210
+
211
+ # Continue with existing fix logic
212
+ if "answer" not in response or not isinstance(response["answer"], dict):
213
+ if verbose:
214
+ print("Cannot fix response: 'answer' field missing or not a dictionary")
215
+ return response
216
+
217
+ answer_dict = response["answer"]
218
+ fixed_answer = {}
219
+
220
+ # Try to convert values to expected types
221
+ for key, type_str in zip(self.answer_keys, getattr(self, "value_types", [])):
222
+ if key in answer_dict:
223
+ value = answer_dict[key]
224
+ # Try type conversion based on the expected type
225
+ if type_str == "int" and not isinstance(value, int):
226
+ try:
227
+ fixed_answer[key] = int(value)
228
+ if verbose:
229
+ print(f"Converted '{key}' from {type(value).__name__} to int")
230
+ continue
231
+ except (ValueError, TypeError):
232
+ pass
233
+
234
+ elif type_str == "float" and not isinstance(value, float):
235
+ try:
236
+ fixed_answer[key] = float(value)
237
+ if verbose:
238
+ print(f"Converted '{key}' from {type(value).__name__} to float")
239
+ continue
240
+ except (ValueError, TypeError):
241
+ pass
242
+
243
+ elif type_str.startswith("list[") and not isinstance(value, list):
244
+ # Try to convert string to list by splitting
245
+ if isinstance(value, str):
246
+ items = [item.strip() for item in value.split(",")]
247
+ fixed_answer[key] = items
248
+ if verbose:
249
+ print(f"Converted '{key}' from string to list: {items}")
250
+ continue
251
+
252
+ # If no conversion needed or possible, keep original
253
+ fixed_answer[key] = value
254
+
255
+ # Preserve any keys we didn't try to fix
256
+ for key, value in answer_dict.items():
257
+ if key not in fixed_answer:
258
+ fixed_answer[key] = value
259
+
260
+ # Return fixed response
261
+ fixed_response = {
262
+ "answer": fixed_answer,
263
+ "comment": response.get("comment"),
264
+ "generated_tokens": response.get("generated_tokens")
265
+ }
266
+
267
+ try:
268
+ # Validate the fixed answer
269
+ self.response_model.model_validate(fixed_response)
270
+ if verbose:
271
+ print("Successfully fixed response")
272
+ return fixed_response
273
+ except Exception as e:
274
+ if verbose:
275
+ print(f"Validation failed for fixed answer: {e}")
276
+ return response
277
+
104
278
  valid_examples = [
105
279
  (
106
280
  {