edsl 0.1.50__py3-none-any.whl → 0.1.52__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- edsl/__init__.py +45 -34
- edsl/__version__.py +1 -1
- edsl/base/base_exception.py +2 -2
- edsl/buckets/bucket_collection.py +1 -1
- edsl/buckets/exceptions.py +32 -0
- edsl/buckets/token_bucket_api.py +26 -10
- edsl/caching/cache.py +5 -2
- edsl/caching/remote_cache_sync.py +5 -5
- edsl/caching/sql_dict.py +12 -11
- edsl/config/__init__.py +1 -1
- edsl/config/config_class.py +4 -2
- edsl/conversation/Conversation.py +9 -5
- edsl/conversation/car_buying.py +1 -3
- edsl/conversation/mug_negotiation.py +2 -6
- edsl/coop/__init__.py +11 -8
- edsl/coop/coop.py +15 -13
- edsl/coop/coop_functions.py +1 -1
- edsl/coop/ep_key_handling.py +1 -1
- edsl/coop/price_fetcher.py +2 -2
- edsl/coop/utils.py +2 -2
- edsl/dataset/dataset.py +144 -63
- edsl/dataset/dataset_operations_mixin.py +14 -6
- edsl/dataset/dataset_tree.py +3 -3
- edsl/dataset/display/table_renderers.py +6 -3
- edsl/dataset/file_exports.py +4 -4
- edsl/dataset/r/ggplot.py +3 -3
- edsl/inference_services/available_model_fetcher.py +2 -2
- edsl/inference_services/data_structures.py +5 -5
- edsl/inference_services/inference_service_abc.py +1 -1
- edsl/inference_services/inference_services_collection.py +1 -1
- edsl/inference_services/service_availability.py +3 -3
- edsl/inference_services/services/azure_ai.py +3 -3
- edsl/inference_services/services/google_service.py +1 -1
- edsl/inference_services/services/test_service.py +1 -1
- edsl/instructions/change_instruction.py +5 -4
- edsl/instructions/instruction.py +1 -0
- edsl/instructions/instruction_collection.py +5 -4
- edsl/instructions/instruction_handler.py +10 -8
- edsl/interviews/answering_function.py +20 -21
- edsl/interviews/exception_tracking.py +3 -2
- edsl/interviews/interview.py +1 -1
- edsl/interviews/interview_status_dictionary.py +1 -1
- edsl/interviews/interview_task_manager.py +7 -4
- edsl/interviews/request_token_estimator.py +3 -2
- edsl/interviews/statistics.py +2 -2
- edsl/invigilators/invigilators.py +34 -6
- edsl/jobs/__init__.py +39 -2
- edsl/jobs/async_interview_runner.py +1 -1
- edsl/jobs/check_survey_scenario_compatibility.py +5 -5
- edsl/jobs/data_structures.py +2 -2
- edsl/jobs/html_table_job_logger.py +494 -257
- edsl/jobs/jobs.py +2 -2
- edsl/jobs/jobs_checks.py +5 -5
- edsl/jobs/jobs_component_constructor.py +2 -2
- edsl/jobs/jobs_pricing_estimation.py +1 -1
- edsl/jobs/jobs_runner_asyncio.py +2 -2
- edsl/jobs/jobs_status_enums.py +1 -0
- edsl/jobs/remote_inference.py +47 -13
- edsl/jobs/results_exceptions_handler.py +2 -2
- edsl/language_models/language_model.py +151 -145
- edsl/notebooks/__init__.py +24 -1
- edsl/notebooks/exceptions.py +82 -0
- edsl/notebooks/notebook.py +7 -3
- edsl/notebooks/notebook_to_latex.py +1 -1
- edsl/prompts/__init__.py +23 -2
- edsl/prompts/prompt.py +1 -1
- edsl/questions/__init__.py +4 -4
- edsl/questions/answer_validator_mixin.py +0 -5
- edsl/questions/compose_questions.py +2 -2
- edsl/questions/descriptors.py +1 -1
- edsl/questions/question_base.py +32 -3
- edsl/questions/question_base_prompts_mixin.py +4 -4
- edsl/questions/question_budget.py +503 -102
- edsl/questions/question_check_box.py +658 -156
- edsl/questions/question_dict.py +176 -2
- edsl/questions/question_extract.py +401 -61
- edsl/questions/question_free_text.py +77 -9
- edsl/questions/question_functional.py +118 -9
- edsl/questions/{derived/question_likert_five.py → question_likert_five.py} +2 -2
- edsl/questions/{derived/question_linear_scale.py → question_linear_scale.py} +3 -4
- edsl/questions/question_list.py +246 -26
- edsl/questions/question_matrix.py +586 -73
- edsl/questions/question_multiple_choice.py +213 -47
- edsl/questions/question_numerical.py +360 -29
- edsl/questions/question_rank.py +401 -124
- edsl/questions/question_registry.py +3 -3
- edsl/questions/{derived/question_top_k.py → question_top_k.py} +3 -3
- edsl/questions/{derived/question_yes_no.py → question_yes_no.py} +3 -4
- edsl/questions/register_questions_meta.py +2 -1
- edsl/questions/response_validator_abc.py +6 -2
- edsl/questions/response_validator_factory.py +10 -12
- edsl/results/report.py +1 -1
- edsl/results/result.py +7 -4
- edsl/results/results.py +500 -271
- edsl/results/results_selector.py +2 -2
- edsl/scenarios/construct_download_link.py +3 -3
- edsl/scenarios/scenario.py +1 -2
- edsl/scenarios/scenario_list.py +41 -23
- edsl/surveys/survey_css.py +3 -3
- edsl/surveys/survey_simulator.py +2 -1
- edsl/tasks/__init__.py +22 -2
- edsl/tasks/exceptions.py +72 -0
- edsl/tasks/task_history.py +48 -11
- edsl/templates/error_reporting/base.html +37 -4
- edsl/templates/error_reporting/exceptions_table.html +105 -33
- edsl/templates/error_reporting/interview_details.html +130 -126
- edsl/templates/error_reporting/overview.html +21 -25
- edsl/templates/error_reporting/report.css +215 -46
- edsl/templates/error_reporting/report.js +122 -20
- edsl/tokens/__init__.py +27 -1
- edsl/tokens/exceptions.py +37 -0
- edsl/tokens/interview_token_usage.py +3 -2
- edsl/tokens/token_usage.py +4 -3
- {edsl-0.1.50.dist-info → edsl-0.1.52.dist-info}/METADATA +1 -1
- {edsl-0.1.50.dist-info → edsl-0.1.52.dist-info}/RECORD +118 -116
- edsl/questions/derived/__init__.py +0 -0
- {edsl-0.1.50.dist-info → edsl-0.1.52.dist-info}/LICENSE +0 -0
- {edsl-0.1.50.dist-info → edsl-0.1.52.dist-info}/WHEEL +0 -0
- {edsl-0.1.50.dist-info → edsl-0.1.52.dist-info}/entry_points.txt +0 -0
edsl/questions/question_dict.py
CHANGED
@@ -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
|
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
|
-
"""
|
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
|
{
|