edsl 0.1.31.dev4__py3-none-any.whl → 0.1.32__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/Invigilator.py +3 -4
- edsl/agents/PromptConstructionMixin.py +35 -15
- edsl/config.py +11 -1
- edsl/conjure/Conjure.py +6 -0
- edsl/data/CacheHandler.py +3 -4
- edsl/enums.py +4 -0
- edsl/exceptions/general.py +10 -8
- edsl/inference_services/AwsBedrock.py +110 -0
- edsl/inference_services/AzureAI.py +197 -0
- edsl/inference_services/DeepInfraService.py +4 -3
- edsl/inference_services/GroqService.py +3 -4
- edsl/inference_services/InferenceServicesCollection.py +13 -8
- edsl/inference_services/OllamaService.py +18 -0
- edsl/inference_services/OpenAIService.py +23 -18
- edsl/inference_services/models_available_cache.py +31 -0
- edsl/inference_services/registry.py +13 -1
- edsl/jobs/Jobs.py +100 -19
- edsl/jobs/buckets/TokenBucket.py +12 -4
- edsl/jobs/interviews/Interview.py +31 -9
- edsl/jobs/interviews/InterviewExceptionEntry.py +101 -0
- edsl/jobs/interviews/InterviewTaskBuildingMixin.py +49 -34
- edsl/jobs/interviews/interview_exception_tracking.py +68 -10
- edsl/jobs/runners/JobsRunnerAsyncio.py +36 -15
- edsl/jobs/runners/JobsRunnerStatusMixin.py +81 -51
- edsl/jobs/tasks/TaskCreators.py +1 -1
- edsl/jobs/tasks/TaskHistory.py +145 -1
- edsl/language_models/LanguageModel.py +58 -43
- edsl/language_models/registry.py +2 -2
- edsl/questions/QuestionBudget.py +0 -1
- edsl/questions/QuestionCheckBox.py +0 -1
- edsl/questions/QuestionExtract.py +0 -1
- edsl/questions/QuestionFreeText.py +2 -9
- edsl/questions/QuestionList.py +0 -1
- edsl/questions/QuestionMultipleChoice.py +1 -2
- edsl/questions/QuestionNumerical.py +0 -1
- edsl/questions/QuestionRank.py +0 -1
- edsl/results/DatasetExportMixin.py +33 -3
- edsl/scenarios/Scenario.py +14 -0
- edsl/scenarios/ScenarioList.py +216 -13
- edsl/scenarios/ScenarioListExportMixin.py +15 -4
- edsl/scenarios/ScenarioListPdfMixin.py +3 -0
- edsl/surveys/Rule.py +5 -2
- edsl/surveys/Survey.py +84 -1
- edsl/surveys/SurveyQualtricsImport.py +213 -0
- edsl/utilities/utilities.py +31 -0
- {edsl-0.1.31.dev4.dist-info → edsl-0.1.32.dist-info}/METADATA +4 -1
- {edsl-0.1.31.dev4.dist-info → edsl-0.1.32.dist-info}/RECORD +50 -45
- {edsl-0.1.31.dev4.dist-info → edsl-0.1.32.dist-info}/LICENSE +0 -0
- {edsl-0.1.31.dev4.dist-info → edsl-0.1.32.dist-info}/WHEEL +0 -0
edsl/jobs/tasks/TaskHistory.py
CHANGED
@@ -11,6 +11,8 @@ class TaskHistory:
|
|
11
11
|
|
12
12
|
[Interview.exceptions, Interview.exceptions, Interview.exceptions, ...]
|
13
13
|
|
14
|
+
>>> _ = TaskHistory.example()
|
15
|
+
...
|
14
16
|
"""
|
15
17
|
|
16
18
|
self.total_interviews = interviews
|
@@ -18,8 +20,26 @@ class TaskHistory:
|
|
18
20
|
|
19
21
|
self._interviews = {index: i for index, i in enumerate(self.total_interviews)}
|
20
22
|
|
23
|
+
@classmethod
|
24
|
+
def example(cls):
|
25
|
+
from edsl.jobs.interviews.Interview import Interview
|
26
|
+
|
27
|
+
from edsl.jobs.Jobs import Jobs
|
28
|
+
|
29
|
+
j = Jobs.example(throw_exception_probability=1, test_model=True)
|
30
|
+
|
31
|
+
from edsl.config import CONFIG
|
32
|
+
|
33
|
+
results = j.run(print_exceptions=False, skip_retry=True, cache=False)
|
34
|
+
|
35
|
+
return cls(results.task_history.total_interviews)
|
36
|
+
|
21
37
|
@property
|
22
38
|
def exceptions(self):
|
39
|
+
"""
|
40
|
+
>>> len(TaskHistory.example().exceptions)
|
41
|
+
4
|
42
|
+
"""
|
23
43
|
return [i.exceptions for k, i in self._interviews.items() if i.exceptions != {}]
|
24
44
|
|
25
45
|
@property
|
@@ -42,7 +62,12 @@ class TaskHistory:
|
|
42
62
|
|
43
63
|
@property
|
44
64
|
def has_exceptions(self) -> bool:
|
45
|
-
"""Return True if there are any exceptions.
|
65
|
+
"""Return True if there are any exceptions.
|
66
|
+
|
67
|
+
>>> TaskHistory.example().has_exceptions
|
68
|
+
True
|
69
|
+
|
70
|
+
"""
|
46
71
|
return len(self.exceptions) > 0
|
47
72
|
|
48
73
|
def _repr_html_(self):
|
@@ -216,6 +241,47 @@ class TaskHistory:
|
|
216
241
|
}
|
217
242
|
"""
|
218
243
|
|
244
|
+
@property
|
245
|
+
def exceptions_by_type(self) -> dict:
|
246
|
+
"""Return a dictionary of exceptions by type."""
|
247
|
+
exceptions_by_type = {}
|
248
|
+
for interview in self.total_interviews:
|
249
|
+
for question_name, exceptions in interview.exceptions.items():
|
250
|
+
for exception in exceptions:
|
251
|
+
exception_type = exception["exception"]
|
252
|
+
if exception_type in exceptions_by_type:
|
253
|
+
exceptions_by_type[exception_type] += 1
|
254
|
+
else:
|
255
|
+
exceptions_by_type[exception_type] = 1
|
256
|
+
return exceptions_by_type
|
257
|
+
|
258
|
+
@property
|
259
|
+
def exceptions_by_question_name(self) -> dict:
|
260
|
+
"""Return a dictionary of exceptions tallied by question name."""
|
261
|
+
exceptions_by_question_name = {}
|
262
|
+
for interview in self.total_interviews:
|
263
|
+
for question_name, exceptions in interview.exceptions.items():
|
264
|
+
if question_name not in exceptions_by_question_name:
|
265
|
+
exceptions_by_question_name[question_name] = 0
|
266
|
+
exceptions_by_question_name[question_name] += len(exceptions)
|
267
|
+
|
268
|
+
for question in self.total_interviews[0].survey.questions:
|
269
|
+
if question.question_name not in exceptions_by_question_name:
|
270
|
+
exceptions_by_question_name[question.question_name] = 0
|
271
|
+
return exceptions_by_question_name
|
272
|
+
|
273
|
+
@property
|
274
|
+
def exceptions_by_model(self) -> dict:
|
275
|
+
"""Return a dictionary of exceptions tallied by model and question name."""
|
276
|
+
exceptions_by_model = {}
|
277
|
+
for interview in self.total_interviews:
|
278
|
+
model = interview.model
|
279
|
+
if model not in exceptions_by_model:
|
280
|
+
exceptions_by_model[model.model] = 0
|
281
|
+
if interview.exceptions != {}:
|
282
|
+
exceptions_by_model[model.model] += len(interview.exceptions)
|
283
|
+
return exceptions_by_model
|
284
|
+
|
219
285
|
def html(
|
220
286
|
self,
|
221
287
|
filename: Optional[str] = None,
|
@@ -236,6 +302,8 @@ class TaskHistory:
|
|
236
302
|
if css is None:
|
237
303
|
css = self.css()
|
238
304
|
|
305
|
+
models_used = set([i.model for index, i in self._interviews.items()])
|
306
|
+
|
239
307
|
template = Template(
|
240
308
|
"""
|
241
309
|
<!DOCTYPE html>
|
@@ -249,6 +317,69 @@ class TaskHistory:
|
|
249
317
|
</style>
|
250
318
|
</head>
|
251
319
|
<body>
|
320
|
+
<h1>Overview</h1>
|
321
|
+
<p>There were {{ interviews|length }} total interviews. The number of interviews with exceptions was {{ num_exceptions }}.</p>
|
322
|
+
<p>The models used were: {{ models_used }}.</p>
|
323
|
+
<p>For documentation on dealing with exceptions on Expected Parrot,
|
324
|
+
see <a href="https://docs.expectedparrot.com/en/latest/exceptions.html">here</a>.</p>
|
325
|
+
|
326
|
+
<h2>Exceptions by Type</h2>
|
327
|
+
<table>
|
328
|
+
<thead>
|
329
|
+
<tr>
|
330
|
+
<th>Exception Type</th>
|
331
|
+
<th>Number</th>
|
332
|
+
</tr>
|
333
|
+
</thead>
|
334
|
+
<tbody>
|
335
|
+
{% for exception_type, exceptions in exceptions_by_type.items() %}
|
336
|
+
<tr>
|
337
|
+
<td>{{ exception_type }}</td>
|
338
|
+
<td>{{ exceptions }}</td>
|
339
|
+
</tr>
|
340
|
+
{% endfor %}
|
341
|
+
</tbody>
|
342
|
+
</table>
|
343
|
+
|
344
|
+
|
345
|
+
<h2>Exceptions by Model</h2>
|
346
|
+
<table>
|
347
|
+
<thead>
|
348
|
+
<tr>
|
349
|
+
<th>Model</th>
|
350
|
+
<th>Number</th>
|
351
|
+
</tr>
|
352
|
+
</thead>
|
353
|
+
<tbody>
|
354
|
+
{% for model, exceptions in exceptions_by_model.items() %}
|
355
|
+
<tr>
|
356
|
+
<td>{{ model }}</td>
|
357
|
+
<td>{{ exceptions }}</td>
|
358
|
+
</tr>
|
359
|
+
{% endfor %}
|
360
|
+
</tbody>
|
361
|
+
</table>
|
362
|
+
|
363
|
+
|
364
|
+
<h2>Exceptions by Question Name</h2>
|
365
|
+
<table>
|
366
|
+
<thead>
|
367
|
+
<tr>
|
368
|
+
<th>Question Name</th>
|
369
|
+
<th>Number of Exceptions</th>
|
370
|
+
</tr>
|
371
|
+
</thead>
|
372
|
+
<tbody>
|
373
|
+
{% for question_name, exception_count in exceptions_by_question_name.items() %}
|
374
|
+
<tr>
|
375
|
+
<td>{{ question_name }}</td>
|
376
|
+
<td>{{ exception_count }}</td>
|
377
|
+
</tr>
|
378
|
+
{% endfor %}
|
379
|
+
</tbody>
|
380
|
+
</table>
|
381
|
+
|
382
|
+
|
252
383
|
{% for index, interview in interviews.items() %}
|
253
384
|
{% if interview.exceptions != {} %}
|
254
385
|
<div class="interview">Interview: {{ index }} </div>
|
@@ -296,11 +427,18 @@ class TaskHistory:
|
|
296
427
|
"""
|
297
428
|
)
|
298
429
|
|
430
|
+
# breakpoint()
|
431
|
+
|
299
432
|
# Render the template with data
|
300
433
|
output = template.render(
|
301
434
|
interviews=self._interviews,
|
302
435
|
css=css,
|
436
|
+
num_exceptions=len(self.exceptions),
|
303
437
|
performance_plot_html=performance_plot_html,
|
438
|
+
exceptions_by_type=self.exceptions_by_type,
|
439
|
+
exceptions_by_question_name=self.exceptions_by_question_name,
|
440
|
+
exceptions_by_model=self.exceptions_by_model,
|
441
|
+
models_used=models_used,
|
304
442
|
)
|
305
443
|
|
306
444
|
# Save the rendered output to a file
|
@@ -344,3 +482,9 @@ class TaskHistory:
|
|
344
482
|
|
345
483
|
if return_link:
|
346
484
|
return filename
|
485
|
+
|
486
|
+
|
487
|
+
if __name__ == "__main__":
|
488
|
+
import doctest
|
489
|
+
|
490
|
+
doctest.testmod(optionflags=doctest.ELLIPSIS)
|
@@ -11,6 +11,7 @@ import hashlib
|
|
11
11
|
from typing import Coroutine, Any, Callable, Type, List, get_type_hints
|
12
12
|
from abc import ABC, abstractmethod
|
13
13
|
|
14
|
+
|
14
15
|
class IntendedModelCallOutcome:
|
15
16
|
"This is a tuple-like class that holds the response, cache_used, and cache_key."
|
16
17
|
|
@@ -21,7 +22,7 @@ class IntendedModelCallOutcome:
|
|
21
22
|
|
22
23
|
def __iter__(self):
|
23
24
|
"""Iterate over the class attributes.
|
24
|
-
|
25
|
+
|
25
26
|
>>> a, b, c = IntendedModelCallOutcome({'answer': "yes"}, True, 'x1289')
|
26
27
|
>>> a
|
27
28
|
{'answer': 'yes'}
|
@@ -32,10 +33,11 @@ class IntendedModelCallOutcome:
|
|
32
33
|
|
33
34
|
def __len__(self):
|
34
35
|
return 3
|
35
|
-
|
36
|
+
|
36
37
|
def __repr__(self):
|
37
38
|
return f"IntendedModelCallOutcome(response = {self.response}, cache_used = {self.cache_used}, cache_key = '{self.cache_key}')"
|
38
39
|
|
40
|
+
|
39
41
|
from edsl.config import CONFIG
|
40
42
|
|
41
43
|
from edsl.utilities.decorators import sync_wrapper, jupyter_nb_handler
|
@@ -131,18 +133,22 @@ class LanguageModel(
|
|
131
133
|
def api_token(self) -> str:
|
132
134
|
if not hasattr(self, "_api_token"):
|
133
135
|
key_name = service_to_api_keyname.get(self._inference_service_, "NOT FOUND")
|
134
|
-
|
135
|
-
if
|
136
|
-
self._api_token
|
137
|
-
|
138
|
-
|
139
|
-
|
136
|
+
|
137
|
+
if self._inference_service_ == "bedrock":
|
138
|
+
self._api_token = [os.getenv(key_name[0]), os.getenv(key_name[1])]
|
139
|
+
# Check if any of the tokens are None
|
140
|
+
missing_token = any(token is None for token in self._api_token)
|
141
|
+
else:
|
142
|
+
self._api_token = os.getenv(key_name)
|
143
|
+
missing_token = self._api_token is None
|
144
|
+
if missing_token and self._inference_service_ != "test" and not self.remote:
|
145
|
+
print("rainsing error")
|
140
146
|
raise MissingAPIKeyError(
|
141
147
|
f"""The key for service: `{self._inference_service_}` is not set.
|
142
|
-
|
143
|
-
"""
|
148
|
+
Need a key with name {key_name} in your .env file."""
|
144
149
|
)
|
145
|
-
|
150
|
+
|
151
|
+
return self._api_token
|
146
152
|
|
147
153
|
def __getitem__(self, key):
|
148
154
|
return getattr(self, key)
|
@@ -159,8 +165,7 @@ class LanguageModel(
|
|
159
165
|
if verbose:
|
160
166
|
print(f"Current key is {masked}")
|
161
167
|
return self.execute_model_call(
|
162
|
-
user_prompt="Hello, model!",
|
163
|
-
system_prompt="You are a helpful agent."
|
168
|
+
user_prompt="Hello, model!", system_prompt="You are a helpful agent."
|
164
169
|
)
|
165
170
|
|
166
171
|
def has_valid_api_key(self) -> bool:
|
@@ -208,8 +213,6 @@ class LanguageModel(
|
|
208
213
|
"""
|
209
214
|
self._set_rate_limits(rpm=rpm, tpm=tpm)
|
210
215
|
|
211
|
-
|
212
|
-
|
213
216
|
def _set_rate_limits(self, rpm=None, tpm=None) -> None:
|
214
217
|
"""Set the rate limits for the model.
|
215
218
|
|
@@ -250,14 +253,16 @@ class LanguageModel(
|
|
250
253
|
>>> LanguageModel._overide_default_parameters(passed_parameter_dict={"temperature": 0.5}, default_parameter_dict={"temperature":0.9, "max_tokens": 1000})
|
251
254
|
{'temperature': 0.5, 'max_tokens': 1000}
|
252
255
|
"""
|
253
|
-
#parameters = dict({})
|
256
|
+
# parameters = dict({})
|
254
257
|
|
255
|
-
return {
|
256
|
-
|
257
|
-
|
258
|
-
|
258
|
+
return {
|
259
|
+
parameter_name: passed_parameter_dict.get(parameter_name, default_value)
|
260
|
+
for parameter_name, default_value in default_parameter_dict.items()
|
261
|
+
}
|
262
|
+
|
263
|
+
def __call__(self, user_prompt: str, system_prompt: str):
|
259
264
|
return self.execute_model_call(user_prompt, system_prompt)
|
260
|
-
|
265
|
+
|
261
266
|
@abstractmethod
|
262
267
|
async def async_execute_model_call(user_prompt: str, system_prompt: str):
|
263
268
|
"""Execute the model call and returns a coroutine.
|
@@ -316,8 +321,10 @@ class LanguageModel(
|
|
316
321
|
data["choices[0]"]["message"]["content"].
|
317
322
|
"""
|
318
323
|
raise NotImplementedError
|
319
|
-
|
320
|
-
async def _async_prepare_response(
|
324
|
+
|
325
|
+
async def _async_prepare_response(
|
326
|
+
self, model_call_outcome: IntendedModelCallOutcome, cache: "Cache"
|
327
|
+
) -> dict:
|
321
328
|
"""Prepare the response for return."""
|
322
329
|
|
323
330
|
model_response = {
|
@@ -327,21 +334,19 @@ class LanguageModel(
|
|
327
334
|
"raw_model_response": model_call_outcome.response,
|
328
335
|
}
|
329
336
|
|
330
|
-
answer_portion = self.parse_response(model_call_outcome.response)
|
337
|
+
answer_portion = self.parse_response(model_call_outcome.response)
|
331
338
|
try:
|
332
339
|
answer_dict = json.loads(answer_portion)
|
333
340
|
except json.JSONDecodeError as e:
|
334
341
|
# TODO: Turn into logs to generate issues
|
335
342
|
answer_dict, success = await repair(
|
336
|
-
bad_json=answer_portion,
|
337
|
-
error_message=str(e),
|
338
|
-
cache=cache
|
343
|
+
bad_json=answer_portion, error_message=str(e), cache=cache
|
339
344
|
)
|
340
345
|
if not success:
|
341
346
|
raise Exception(
|
342
347
|
f"""Even the repair failed. The error was: {e}. The response was: {answer_portion}."""
|
343
348
|
)
|
344
|
-
|
349
|
+
|
345
350
|
return {**model_response, **answer_dict}
|
346
351
|
|
347
352
|
async def async_get_raw_response(
|
@@ -353,16 +358,18 @@ class LanguageModel(
|
|
353
358
|
encoded_image=None,
|
354
359
|
) -> IntendedModelCallOutcome:
|
355
360
|
import warnings
|
356
|
-
|
361
|
+
|
362
|
+
warnings.warn(
|
363
|
+
"This method is deprecated. Use async_get_intended_model_call_outcome."
|
364
|
+
)
|
357
365
|
return await self._async_get_intended_model_call_outcome(
|
358
366
|
user_prompt=user_prompt,
|
359
367
|
system_prompt=system_prompt,
|
360
368
|
cache=cache,
|
361
369
|
iteration=iteration,
|
362
|
-
encoded_image=encoded_image
|
370
|
+
encoded_image=encoded_image,
|
363
371
|
)
|
364
372
|
|
365
|
-
|
366
373
|
async def _async_get_intended_model_call_outcome(
|
367
374
|
self,
|
368
375
|
user_prompt: str,
|
@@ -404,8 +411,8 @@ class LanguageModel(
|
|
404
411
|
"iteration": iteration,
|
405
412
|
}
|
406
413
|
cached_response, cache_key = cache.fetch(**cache_call_params)
|
407
|
-
|
408
|
-
if
|
414
|
+
|
415
|
+
if cache_used := cached_response is not None:
|
409
416
|
response = json.loads(cached_response)
|
410
417
|
else:
|
411
418
|
f = (
|
@@ -413,16 +420,24 @@ class LanguageModel(
|
|
413
420
|
if hasattr(self, "remote") and self.remote
|
414
421
|
else self.async_execute_model_call
|
415
422
|
)
|
416
|
-
params = {
|
417
|
-
|
423
|
+
params = {
|
424
|
+
"user_prompt": user_prompt,
|
425
|
+
"system_prompt": system_prompt,
|
426
|
+
**({"encoded_image": encoded_image} if encoded_image else {}),
|
418
427
|
}
|
419
428
|
response = await f(**params)
|
420
|
-
new_cache_key = cache.store(
|
421
|
-
|
422
|
-
|
423
|
-
|
429
|
+
new_cache_key = cache.store(
|
430
|
+
**cache_call_params, response=response
|
431
|
+
) # store the response in the cache
|
432
|
+
assert new_cache_key == cache_key # should be the same
|
433
|
+
|
434
|
+
return IntendedModelCallOutcome(
|
435
|
+
response=response, cache_used=cache_used, cache_key=cache_key
|
436
|
+
)
|
424
437
|
|
425
|
-
_get_intended_model_call_outcome = sync_wrapper(
|
438
|
+
_get_intended_model_call_outcome = sync_wrapper(
|
439
|
+
_async_get_intended_model_call_outcome
|
440
|
+
)
|
426
441
|
|
427
442
|
get_raw_response = sync_wrapper(async_get_raw_response)
|
428
443
|
|
@@ -443,7 +458,7 @@ class LanguageModel(
|
|
443
458
|
self,
|
444
459
|
user_prompt: str,
|
445
460
|
system_prompt: str,
|
446
|
-
cache:
|
461
|
+
cache: "Cache",
|
447
462
|
iteration: int = 1,
|
448
463
|
encoded_image=None,
|
449
464
|
) -> dict:
|
@@ -461,8 +476,8 @@ class LanguageModel(
|
|
461
476
|
"system_prompt": system_prompt,
|
462
477
|
"iteration": iteration,
|
463
478
|
"cache": cache,
|
464
|
-
**({"encoded_image": encoded_image} if encoded_image else {})
|
465
|
-
}
|
479
|
+
**({"encoded_image": encoded_image} if encoded_image else {}),
|
480
|
+
}
|
466
481
|
model_call_outcome = await self._async_get_intended_model_call_outcome(**params)
|
467
482
|
return await self._async_prepare_response(model_call_outcome, cache=cache)
|
468
483
|
|
edsl/language_models/registry.py
CHANGED
@@ -36,10 +36,10 @@ class Model(metaclass=Meta):
|
|
36
36
|
from edsl.inference_services.registry import default
|
37
37
|
|
38
38
|
registry = registry or default
|
39
|
-
|
39
|
+
|
40
40
|
if isinstance(model_name, int):
|
41
41
|
model_name = cls.available(name_only=True)[model_name]
|
42
|
-
|
42
|
+
|
43
43
|
factory = registry.create_model_factory(model_name)
|
44
44
|
return factory(*args, **kwargs)
|
45
45
|
|
edsl/questions/QuestionBudget.py
CHANGED
@@ -25,7 +25,6 @@ class QuestionBudget(QuestionBase):
|
|
25
25
|
:param question_text: The text of the question.
|
26
26
|
:param question_options: The options for allocation of the budget sum.
|
27
27
|
:param budget_sum: The total amount of the budget to be allocated among the options.
|
28
|
-
:param instructions: Instructions for the question. If not provided, the default instructions are used. To view them, run `QuestionBudget.default_instructions`.
|
29
28
|
"""
|
30
29
|
self.question_name = question_name
|
31
30
|
self.question_text = question_text
|
@@ -33,7 +33,6 @@ class QuestionCheckBox(QuestionBase):
|
|
33
33
|
:param question_name: The name of the question.
|
34
34
|
:param question_text: The text of the question.
|
35
35
|
:param question_options: The options the respondent should select from.
|
36
|
-
:param instructions: Instructions for the question. If not provided, the default instructions are used. To view them, run `QuestionCheckBox.default_instructions`.
|
37
36
|
:param min_selections: The minimum number of options that must be selected.
|
38
37
|
:param max_selections: The maximum number of options that must be selected.
|
39
38
|
"""
|
@@ -22,7 +22,6 @@ class QuestionExtract(QuestionBase):
|
|
22
22
|
:param question_text: The text of the question.
|
23
23
|
:param question_options: The options the respondent should select from.
|
24
24
|
:param answer_template: The template for the answer.
|
25
|
-
:param instructions: Instructions for the question. If not provided, the default instructions are used. To view them, run `QuestionExtract.default_instructions`.
|
26
25
|
"""
|
27
26
|
self.question_name = question_name
|
28
27
|
self.question_text = question_text
|
@@ -17,21 +17,14 @@ class QuestionFreeText(QuestionBase):
|
|
17
17
|
"""
|
18
18
|
)
|
19
19
|
|
20
|
-
def __init__(
|
21
|
-
self,
|
22
|
-
question_name: str,
|
23
|
-
question_text: str,
|
24
|
-
instructions: Optional[str] = None,
|
25
|
-
):
|
20
|
+
def __init__(self, question_name: str, question_text: str):
|
26
21
|
"""Instantiate a new QuestionFreeText.
|
27
22
|
|
28
23
|
:param question_name: The name of the question.
|
29
24
|
:param question_text: The text of the question.
|
30
|
-
:param instructions: Instructions for the question. If not provided, the default instructions are used. To view them, run `QuestionFreeText.default_instructions`.
|
31
25
|
"""
|
32
26
|
self.question_name = question_name
|
33
27
|
self.question_text = question_text
|
34
|
-
self.instructions = instructions
|
35
28
|
|
36
29
|
################
|
37
30
|
# Answer methods
|
@@ -79,7 +72,7 @@ def main():
|
|
79
72
|
q = QuestionFreeText.example()
|
80
73
|
q.question_text
|
81
74
|
q.question_name
|
82
|
-
q.instructions
|
75
|
+
# q.instructions
|
83
76
|
# validate an answer
|
84
77
|
q._validate_answer({"answer": "I like custard"})
|
85
78
|
# translate answer code
|
edsl/questions/QuestionList.py
CHANGED
@@ -22,7 +22,6 @@ class QuestionList(QuestionBase):
|
|
22
22
|
|
23
23
|
:param question_name: The name of the question.
|
24
24
|
:param question_text: The text of the question.
|
25
|
-
:param instructions: Instructions for the question. If not provided, the default instructions are used. To view them, run `QuestionList.default_instructions`.
|
26
25
|
:param max_list_items: The maximum number of items that can be in the answer list.
|
27
26
|
"""
|
28
27
|
self.question_name = question_name
|
@@ -33,7 +33,6 @@ class QuestionMultipleChoice(QuestionBase):
|
|
33
33
|
:param question_name: The name of the question.
|
34
34
|
:param question_text: The text of the question.
|
35
35
|
:param question_options: The options the agent should select from.
|
36
|
-
:param instructions: Instructions for the question. If not provided, the default instructions are used. To view them, run `QuestionMultipleChoice.default_instructions`.
|
37
36
|
"""
|
38
37
|
self.question_name = question_name
|
39
38
|
self.question_text = question_text
|
@@ -96,7 +95,7 @@ class QuestionMultipleChoice(QuestionBase):
|
|
96
95
|
question_option_key = list(meta.find_undeclared_variables(parsed_content))[
|
97
96
|
0
|
98
97
|
]
|
99
|
-
#breakpoint()
|
98
|
+
# breakpoint()
|
100
99
|
translated_options = scenario.get(question_option_key)
|
101
100
|
else:
|
102
101
|
translated_options = [
|
@@ -26,7 +26,6 @@ class QuestionNumerical(QuestionBase):
|
|
26
26
|
|
27
27
|
:param question_name: The name of the question.
|
28
28
|
:param question_text: The text of the question.
|
29
|
-
:param instructions: Instructions for the question. If not provided, the default instructions are used. To view them, run `QuestionNumerical.default_instructions`.
|
30
29
|
:param min_value: The minimum value of the answer.
|
31
30
|
:param max_value: The maximum value of the answer.
|
32
31
|
"""
|
edsl/questions/QuestionRank.py
CHANGED
@@ -30,7 +30,6 @@ class QuestionRank(QuestionBase):
|
|
30
30
|
:param question_name: The name of the question.
|
31
31
|
:param question_text: The text of the question.
|
32
32
|
:param question_options: The options the respondent should select from.
|
33
|
-
:param instructions: Instructions for the question. If not provided, the default instructions are used. To view them, run `QuestionRank.default_instructions`.
|
34
33
|
:param min_selections: The minimum number of options that must be selected.
|
35
34
|
:param max_selections: The maximum number of options that must be selected.
|
36
35
|
"""
|
@@ -27,6 +27,10 @@ class DatasetExportMixin:
|
|
27
27
|
>>> d.relevant_columns(remove_prefix=True)
|
28
28
|
['b']
|
29
29
|
|
30
|
+
>>> d = Dataset([{'a':[1,2,3,4]}, {'b':[5,6,7,8]}])
|
31
|
+
>>> d.relevant_columns()
|
32
|
+
['a', 'b']
|
33
|
+
|
30
34
|
>>> from edsl.results import Results; Results.example().select('how_feeling', 'how_feeling_yesterday').relevant_columns()
|
31
35
|
['answer.how_feeling', 'answer.how_feeling_yesterday']
|
32
36
|
|
@@ -592,8 +596,29 @@ class DatasetExportMixin:
|
|
592
596
|
if return_link:
|
593
597
|
return filename
|
594
598
|
|
599
|
+
def to_docx(self, filename: Optional[str] = None, separator: str = "\n"):
|
600
|
+
"""Export the results to a Word document.
|
601
|
+
|
602
|
+
:param filename: The filename to save the Word document to.
|
603
|
+
|
604
|
+
|
605
|
+
"""
|
606
|
+
from docx import Document
|
607
|
+
|
608
|
+
doc = Document()
|
609
|
+
for entry in self:
|
610
|
+
key, values = list(entry.items())[0]
|
611
|
+
doc.add_paragraph(key)
|
612
|
+
line = separator.join(values)
|
613
|
+
doc.add_paragraph(line)
|
614
|
+
|
615
|
+
if filename is not None:
|
616
|
+
doc.save(filename)
|
617
|
+
else:
|
618
|
+
return doc
|
619
|
+
|
595
620
|
def tally(
|
596
|
-
self, *fields: Optional[str], top_n: Optional[int] = None, output="
|
621
|
+
self, *fields: Optional[str], top_n: Optional[int] = None, output="Dataset"
|
597
622
|
) -> Union[dict, "Dataset"]:
|
598
623
|
"""Tally the values of a field or perform a cross-tab of multiple fields.
|
599
624
|
|
@@ -601,9 +626,11 @@ class DatasetExportMixin:
|
|
601
626
|
|
602
627
|
>>> from edsl.results import Results
|
603
628
|
>>> r = Results.example()
|
604
|
-
>>> r.select('how_feeling').tally('answer.how_feeling')
|
629
|
+
>>> r.select('how_feeling').tally('answer.how_feeling', output = "dict")
|
605
630
|
{'OK': 2, 'Great': 1, 'Terrible': 1}
|
606
|
-
>>> r.select('how_feeling'
|
631
|
+
>>> r.select('how_feeling').tally('answer.how_feeling', output = "Dataset")
|
632
|
+
Dataset([{'value': ['OK', 'Great', 'Terrible']}, {'count': [2, 1, 1]}])
|
633
|
+
>>> r.select('how_feeling', 'period').tally('how_feeling', 'period', output = "dict")
|
607
634
|
{('OK', 'morning'): 1, ('Great', 'afternoon'): 1, ('Terrible', 'morning'): 1, ('OK', 'afternoon'): 1}
|
608
635
|
"""
|
609
636
|
from collections import Counter
|
@@ -615,6 +642,8 @@ class DatasetExportMixin:
|
|
615
642
|
column.split(".")[-1] for column in self.relevant_columns()
|
616
643
|
]
|
617
644
|
|
645
|
+
# breakpoint()
|
646
|
+
|
618
647
|
if not all(
|
619
648
|
f in self.relevant_columns() or f in relevant_columns_without_prefix
|
620
649
|
for f in fields
|
@@ -641,6 +670,7 @@ class DatasetExportMixin:
|
|
641
670
|
from edsl.results.Dataset import Dataset
|
642
671
|
|
643
672
|
if output == "dict":
|
673
|
+
# why did I do this?
|
644
674
|
warnings.warn(
|
645
675
|
textwrap.dedent(
|
646
676
|
"""\
|
edsl/scenarios/Scenario.py
CHANGED
@@ -182,6 +182,19 @@ class Scenario(Base, UserDict, ScenarioImageMixin, ScenarioHtmlMixin):
|
|
182
182
|
new_scenario[key] = self[key]
|
183
183
|
return new_scenario
|
184
184
|
|
185
|
+
@classmethod
|
186
|
+
def from_url(cls, url: str, field_name: Optional[str] = "text") -> "Scenario":
|
187
|
+
"""Creates a scenario from a URL.
|
188
|
+
|
189
|
+
:param url: The URL to create the scenario from.
|
190
|
+
:param field_name: The field name to use for the text.
|
191
|
+
|
192
|
+
"""
|
193
|
+
import requests
|
194
|
+
|
195
|
+
text = requests.get(url).text
|
196
|
+
return cls({"url": url, field_name: text})
|
197
|
+
|
185
198
|
@classmethod
|
186
199
|
def from_image(cls, image_path: str) -> str:
|
187
200
|
"""Creates a scenario with a base64 encoding of an image.
|
@@ -207,6 +220,7 @@ class Scenario(Base, UserDict, ScenarioImageMixin, ScenarioHtmlMixin):
|
|
207
220
|
@classmethod
|
208
221
|
def from_pdf(cls, pdf_path):
|
209
222
|
import fitz # PyMuPDF
|
223
|
+
from edsl import Scenario
|
210
224
|
|
211
225
|
# Ensure the file exists
|
212
226
|
if not os.path.exists(pdf_path):
|