edsl 0.1.31.dev3__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 +7 -2
- edsl/agents/PromptConstructionMixin.py +35 -15
- edsl/config.py +15 -1
- edsl/conjure/Conjure.py +6 -0
- edsl/coop/coop.py +4 -0
- edsl/data/CacheHandler.py +3 -4
- edsl/enums.py +5 -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 +6 -91
- edsl/inference_services/GroqService.py +18 -0
- edsl/inference_services/InferenceServicesCollection.py +13 -8
- edsl/inference_services/OllamaService.py +18 -0
- edsl/inference_services/OpenAIService.py +68 -21
- edsl/inference_services/models_available_cache.py +31 -0
- edsl/inference_services/registry.py +14 -1
- edsl/jobs/Jobs.py +103 -21
- 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 -33
- edsl/jobs/interviews/interview_exception_tracking.py +68 -10
- edsl/jobs/runners/JobsRunnerAsyncio.py +112 -81
- edsl/jobs/runners/JobsRunnerStatusData.py +0 -237
- edsl/jobs/runners/JobsRunnerStatusMixin.py +291 -35
- edsl/jobs/tasks/TaskCreators.py +8 -2
- edsl/jobs/tasks/TaskHistory.py +145 -1
- edsl/language_models/LanguageModel.py +62 -41
- edsl/language_models/registry.py +4 -0
- 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.dev3.dist-info → edsl-0.1.32.dist-info}/METADATA +5 -1
- {edsl-0.1.31.dev3.dist-info → edsl-0.1.32.dist-info}/RECORD +52 -46
- {edsl-0.1.31.dev3.dist-info → edsl-0.1.32.dist-info}/LICENSE +0 -0
- {edsl-0.1.31.dev3.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
|
@@ -122,22 +124,31 @@ class LanguageModel(
|
|
122
124
|
# Skip the API key check. Sometimes this is useful for testing.
|
123
125
|
self._api_token = None
|
124
126
|
|
127
|
+
def ask_question(self, question):
|
128
|
+
user_prompt = question.get_instructions().render(question.data).text
|
129
|
+
system_prompt = "You are a helpful agent pretending to be a human."
|
130
|
+
return self.execute_model_call(user_prompt, system_prompt)
|
131
|
+
|
125
132
|
@property
|
126
133
|
def api_token(self) -> str:
|
127
134
|
if not hasattr(self, "_api_token"):
|
128
135
|
key_name = service_to_api_keyname.get(self._inference_service_, "NOT FOUND")
|
129
|
-
|
130
|
-
if
|
131
|
-
self._api_token
|
132
|
-
|
133
|
-
|
134
|
-
|
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")
|
135
146
|
raise MissingAPIKeyError(
|
136
147
|
f"""The key for service: `{self._inference_service_}` is not set.
|
137
|
-
|
138
|
-
"""
|
148
|
+
Need a key with name {key_name} in your .env file."""
|
139
149
|
)
|
140
|
-
|
150
|
+
|
151
|
+
return self._api_token
|
141
152
|
|
142
153
|
def __getitem__(self, key):
|
143
154
|
return getattr(self, key)
|
@@ -202,8 +213,6 @@ class LanguageModel(
|
|
202
213
|
"""
|
203
214
|
self._set_rate_limits(rpm=rpm, tpm=tpm)
|
204
215
|
|
205
|
-
|
206
|
-
|
207
216
|
def _set_rate_limits(self, rpm=None, tpm=None) -> None:
|
208
217
|
"""Set the rate limits for the model.
|
209
218
|
|
@@ -244,14 +253,16 @@ class LanguageModel(
|
|
244
253
|
>>> LanguageModel._overide_default_parameters(passed_parameter_dict={"temperature": 0.5}, default_parameter_dict={"temperature":0.9, "max_tokens": 1000})
|
245
254
|
{'temperature': 0.5, 'max_tokens': 1000}
|
246
255
|
"""
|
247
|
-
#parameters = dict({})
|
256
|
+
# parameters = dict({})
|
257
|
+
|
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
|
+
}
|
248
262
|
|
249
|
-
|
250
|
-
for parameter_name, default_value in default_parameter_dict.items()}
|
251
|
-
|
252
|
-
def __call__(self, user_prompt:str, system_prompt:str):
|
263
|
+
def __call__(self, user_prompt: str, system_prompt: str):
|
253
264
|
return self.execute_model_call(user_prompt, system_prompt)
|
254
|
-
|
265
|
+
|
255
266
|
@abstractmethod
|
256
267
|
async def async_execute_model_call(user_prompt: str, system_prompt: str):
|
257
268
|
"""Execute the model call and returns a coroutine.
|
@@ -310,8 +321,10 @@ class LanguageModel(
|
|
310
321
|
data["choices[0]"]["message"]["content"].
|
311
322
|
"""
|
312
323
|
raise NotImplementedError
|
313
|
-
|
314
|
-
async def _async_prepare_response(
|
324
|
+
|
325
|
+
async def _async_prepare_response(
|
326
|
+
self, model_call_outcome: IntendedModelCallOutcome, cache: "Cache"
|
327
|
+
) -> dict:
|
315
328
|
"""Prepare the response for return."""
|
316
329
|
|
317
330
|
model_response = {
|
@@ -321,21 +334,19 @@ class LanguageModel(
|
|
321
334
|
"raw_model_response": model_call_outcome.response,
|
322
335
|
}
|
323
336
|
|
324
|
-
answer_portion = self.parse_response(model_call_outcome.response)
|
337
|
+
answer_portion = self.parse_response(model_call_outcome.response)
|
325
338
|
try:
|
326
339
|
answer_dict = json.loads(answer_portion)
|
327
340
|
except json.JSONDecodeError as e:
|
328
341
|
# TODO: Turn into logs to generate issues
|
329
342
|
answer_dict, success = await repair(
|
330
|
-
bad_json=answer_portion,
|
331
|
-
error_message=str(e),
|
332
|
-
cache=cache
|
343
|
+
bad_json=answer_portion, error_message=str(e), cache=cache
|
333
344
|
)
|
334
345
|
if not success:
|
335
346
|
raise Exception(
|
336
347
|
f"""Even the repair failed. The error was: {e}. The response was: {answer_portion}."""
|
337
348
|
)
|
338
|
-
|
349
|
+
|
339
350
|
return {**model_response, **answer_dict}
|
340
351
|
|
341
352
|
async def async_get_raw_response(
|
@@ -347,16 +358,18 @@ class LanguageModel(
|
|
347
358
|
encoded_image=None,
|
348
359
|
) -> IntendedModelCallOutcome:
|
349
360
|
import warnings
|
350
|
-
|
361
|
+
|
362
|
+
warnings.warn(
|
363
|
+
"This method is deprecated. Use async_get_intended_model_call_outcome."
|
364
|
+
)
|
351
365
|
return await self._async_get_intended_model_call_outcome(
|
352
366
|
user_prompt=user_prompt,
|
353
367
|
system_prompt=system_prompt,
|
354
368
|
cache=cache,
|
355
369
|
iteration=iteration,
|
356
|
-
encoded_image=encoded_image
|
370
|
+
encoded_image=encoded_image,
|
357
371
|
)
|
358
372
|
|
359
|
-
|
360
373
|
async def _async_get_intended_model_call_outcome(
|
361
374
|
self,
|
362
375
|
user_prompt: str,
|
@@ -398,8 +411,8 @@ class LanguageModel(
|
|
398
411
|
"iteration": iteration,
|
399
412
|
}
|
400
413
|
cached_response, cache_key = cache.fetch(**cache_call_params)
|
401
|
-
|
402
|
-
if
|
414
|
+
|
415
|
+
if cache_used := cached_response is not None:
|
403
416
|
response = json.loads(cached_response)
|
404
417
|
else:
|
405
418
|
f = (
|
@@ -407,16 +420,24 @@ class LanguageModel(
|
|
407
420
|
if hasattr(self, "remote") and self.remote
|
408
421
|
else self.async_execute_model_call
|
409
422
|
)
|
410
|
-
params = {
|
411
|
-
|
423
|
+
params = {
|
424
|
+
"user_prompt": user_prompt,
|
425
|
+
"system_prompt": system_prompt,
|
426
|
+
**({"encoded_image": encoded_image} if encoded_image else {}),
|
412
427
|
}
|
413
428
|
response = await f(**params)
|
414
|
-
new_cache_key = cache.store(
|
415
|
-
|
416
|
-
|
417
|
-
|
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
|
+
)
|
418
437
|
|
419
|
-
_get_intended_model_call_outcome = sync_wrapper(
|
438
|
+
_get_intended_model_call_outcome = sync_wrapper(
|
439
|
+
_async_get_intended_model_call_outcome
|
440
|
+
)
|
420
441
|
|
421
442
|
get_raw_response = sync_wrapper(async_get_raw_response)
|
422
443
|
|
@@ -437,7 +458,7 @@ class LanguageModel(
|
|
437
458
|
self,
|
438
459
|
user_prompt: str,
|
439
460
|
system_prompt: str,
|
440
|
-
cache:
|
461
|
+
cache: "Cache",
|
441
462
|
iteration: int = 1,
|
442
463
|
encoded_image=None,
|
443
464
|
) -> dict:
|
@@ -455,8 +476,8 @@ class LanguageModel(
|
|
455
476
|
"system_prompt": system_prompt,
|
456
477
|
"iteration": iteration,
|
457
478
|
"cache": cache,
|
458
|
-
**({"encoded_image": encoded_image} if encoded_image else {})
|
459
|
-
}
|
479
|
+
**({"encoded_image": encoded_image} if encoded_image else {}),
|
480
|
+
}
|
460
481
|
model_call_outcome = await self._async_get_intended_model_call_outcome(**params)
|
461
482
|
return await self._async_prepare_response(model_call_outcome, cache=cache)
|
462
483
|
|
edsl/language_models/registry.py
CHANGED
@@ -36,6 +36,10 @@ class Model(metaclass=Meta):
|
|
36
36
|
from edsl.inference_services.registry import default
|
37
37
|
|
38
38
|
registry = registry or default
|
39
|
+
|
40
|
+
if isinstance(model_name, int):
|
41
|
+
model_name = cls.available(name_only=True)[model_name]
|
42
|
+
|
39
43
|
factory = registry.create_model_factory(model_name)
|
40
44
|
return factory(*args, **kwargs)
|
41
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):
|