edsl 0.1.29.dev3__py3-none-any.whl → 0.1.30__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 (75) hide show
  1. edsl/Base.py +18 -18
  2. edsl/__init__.py +23 -23
  3. edsl/__version__.py +1 -1
  4. edsl/agents/Agent.py +79 -41
  5. edsl/agents/AgentList.py +26 -26
  6. edsl/agents/Invigilator.py +19 -2
  7. edsl/agents/InvigilatorBase.py +15 -10
  8. edsl/agents/PromptConstructionMixin.py +342 -100
  9. edsl/agents/descriptors.py +2 -1
  10. edsl/base/Base.py +289 -0
  11. edsl/config.py +2 -1
  12. edsl/conjure/InputData.py +39 -8
  13. edsl/conversation/car_buying.py +1 -1
  14. edsl/coop/coop.py +187 -150
  15. edsl/coop/utils.py +43 -75
  16. edsl/data/Cache.py +41 -18
  17. edsl/data/CacheEntry.py +6 -7
  18. edsl/data/SQLiteDict.py +11 -3
  19. edsl/data_transfer_models.py +4 -0
  20. edsl/jobs/Answers.py +15 -1
  21. edsl/jobs/Jobs.py +108 -49
  22. edsl/jobs/buckets/ModelBuckets.py +14 -2
  23. edsl/jobs/buckets/TokenBucket.py +32 -5
  24. edsl/jobs/interviews/Interview.py +99 -79
  25. edsl/jobs/interviews/InterviewTaskBuildingMixin.py +19 -24
  26. edsl/jobs/runners/JobsRunnerAsyncio.py +16 -16
  27. edsl/jobs/tasks/QuestionTaskCreator.py +10 -6
  28. edsl/jobs/tasks/TaskHistory.py +4 -3
  29. edsl/language_models/LanguageModel.py +17 -17
  30. edsl/language_models/ModelList.py +1 -1
  31. edsl/language_models/repair.py +8 -7
  32. edsl/notebooks/Notebook.py +47 -10
  33. edsl/prompts/Prompt.py +31 -19
  34. edsl/questions/QuestionBase.py +38 -13
  35. edsl/questions/QuestionBudget.py +5 -6
  36. edsl/questions/QuestionCheckBox.py +7 -3
  37. edsl/questions/QuestionExtract.py +5 -3
  38. edsl/questions/QuestionFreeText.py +7 -5
  39. edsl/questions/QuestionFunctional.py +34 -5
  40. edsl/questions/QuestionList.py +3 -4
  41. edsl/questions/QuestionMultipleChoice.py +68 -12
  42. edsl/questions/QuestionNumerical.py +4 -3
  43. edsl/questions/QuestionRank.py +5 -3
  44. edsl/questions/__init__.py +4 -3
  45. edsl/questions/descriptors.py +46 -4
  46. edsl/questions/question_registry.py +20 -31
  47. edsl/questions/settings.py +1 -1
  48. edsl/results/Dataset.py +31 -0
  49. edsl/results/DatasetExportMixin.py +570 -0
  50. edsl/results/Result.py +66 -70
  51. edsl/results/Results.py +160 -68
  52. edsl/results/ResultsDBMixin.py +7 -3
  53. edsl/results/ResultsExportMixin.py +22 -537
  54. edsl/results/ResultsGGMixin.py +3 -3
  55. edsl/results/ResultsToolsMixin.py +5 -5
  56. edsl/scenarios/FileStore.py +299 -0
  57. edsl/scenarios/Scenario.py +16 -24
  58. edsl/scenarios/ScenarioList.py +42 -17
  59. edsl/scenarios/ScenarioListExportMixin.py +32 -0
  60. edsl/scenarios/ScenarioListPdfMixin.py +2 -1
  61. edsl/scenarios/__init__.py +1 -0
  62. edsl/study/Study.py +8 -16
  63. edsl/surveys/MemoryPlan.py +11 -4
  64. edsl/surveys/Survey.py +88 -17
  65. edsl/surveys/SurveyExportMixin.py +4 -2
  66. edsl/surveys/SurveyFlowVisualizationMixin.py +6 -4
  67. edsl/tools/plotting.py +4 -2
  68. edsl/utilities/__init__.py +21 -21
  69. edsl/utilities/interface.py +66 -45
  70. edsl/utilities/utilities.py +11 -13
  71. {edsl-0.1.29.dev3.dist-info → edsl-0.1.30.dist-info}/METADATA +11 -10
  72. {edsl-0.1.29.dev3.dist-info → edsl-0.1.30.dist-info}/RECORD +74 -71
  73. {edsl-0.1.29.dev3.dist-info → edsl-0.1.30.dist-info}/WHEEL +1 -1
  74. edsl-0.1.29.dev3.dist-info/entry_points.txt +0 -3
  75. {edsl-0.1.29.dev3.dist-info → edsl-0.1.30.dist-info}/LICENSE +0 -0
@@ -7,26 +7,18 @@ import asyncio
7
7
  import json
8
8
  import time
9
9
  import os
10
-
11
10
  from typing import Coroutine, Any, Callable, Type, List, get_type_hints
12
-
13
- from abc import ABC, abstractmethod, ABCMeta
14
-
15
- from rich.table import Table
11
+ from abc import ABC, abstractmethod
16
12
 
17
13
  from edsl.config import CONFIG
18
14
 
19
- from edsl.utilities.utilities import clean_json
20
15
  from edsl.utilities.decorators import sync_wrapper, jupyter_nb_handler
21
16
  from edsl.utilities.decorators import add_edsl_version, remove_edsl_version
17
+
22
18
  from edsl.language_models.repair import repair
23
- from edsl.exceptions.language_models import LanguageModelAttributeTypeError
24
19
  from edsl.enums import InferenceServiceType
25
20
  from edsl.Base import RichPrintingMixin, PersistenceMixin
26
- from edsl.data.Cache import Cache
27
21
  from edsl.enums import service_to_api_keyname
28
-
29
-
30
22
  from edsl.exceptions import MissingAPIKeyError
31
23
  from edsl.language_models.RegisterLanguageModelsMeta import RegisterLanguageModelsMeta
32
24
 
@@ -291,7 +283,7 @@ class LanguageModel(
291
283
  self,
292
284
  user_prompt: str,
293
285
  system_prompt: str,
294
- cache,
286
+ cache: "Cache",
295
287
  iteration: int = 0,
296
288
  encoded_image=None,
297
289
  ) -> tuple[dict, bool, str]:
@@ -331,12 +323,10 @@ class LanguageModel(
331
323
  image_hash = hashlib.md5(encoded_image.encode()).hexdigest()
332
324
  cache_call_params["user_prompt"] = f"{user_prompt} {image_hash}"
333
325
 
334
- cached_response = cache.fetch(**cache_call_params)
335
-
326
+ cached_response, cache_key = cache.fetch(**cache_call_params)
336
327
  if cached_response:
337
328
  response = json.loads(cached_response)
338
329
  cache_used = True
339
- cache_key = None
340
330
  else:
341
331
  remote_call = hasattr(self, "remote") and self.remote
342
332
  f = (
@@ -348,7 +338,7 @@ class LanguageModel(
348
338
  if encoded_image:
349
339
  params["encoded_image"] = encoded_image
350
340
  response = await f(**params)
351
- cache_key = cache.store(
341
+ new_cache_key = cache.store(
352
342
  user_prompt=user_prompt,
353
343
  model=str(self.model),
354
344
  parameters=self.parameters,
@@ -356,6 +346,7 @@ class LanguageModel(
356
346
  response=response,
357
347
  iteration=iteration,
358
348
  )
349
+ assert new_cache_key == cache_key
359
350
  cache_used = False
360
351
 
361
352
  return response, cache_used, cache_key
@@ -420,7 +411,7 @@ class LanguageModel(
420
411
 
421
412
  dict_response.update(
422
413
  {
423
- "cached_used": cache_used,
414
+ "cache_used": cache_used,
424
415
  "cache_key": cache_key,
425
416
  "usage": raw_response.get("usage", {}),
426
417
  "raw_model_response": raw_response,
@@ -490,6 +481,8 @@ class LanguageModel(
490
481
 
491
482
  def rich_print(self):
492
483
  """Display an object as a table."""
484
+ from rich.table import Table
485
+
493
486
  table = Table(title="Language Model")
494
487
  table.add_column("Attribute", style="bold")
495
488
  table.add_column("Value")
@@ -501,7 +494,12 @@ class LanguageModel(
501
494
  return table
502
495
 
503
496
  @classmethod
504
- def example(cls, test_model: bool = False, canned_response: str = "Hello world"):
497
+ def example(
498
+ cls,
499
+ test_model: bool = False,
500
+ canned_response: str = "Hello world",
501
+ throw_exception: bool = False,
502
+ ):
505
503
  """Return a default instance of the class.
506
504
 
507
505
  >>> from edsl.language_models import LanguageModel
@@ -526,6 +524,8 @@ class LanguageModel(
526
524
  ) -> dict[str, Any]:
527
525
  await asyncio.sleep(0.1)
528
526
  # return {"message": """{"answer": "Hello, world"}"""}
527
+ if throw_exception:
528
+ raise Exception("This is a test error")
529
529
  return {"message": f'{{"answer": "{canned_response}"}}'}
530
530
 
531
531
  def parse_response(self, raw_response: dict[str, Any]) -> str:
@@ -5,7 +5,7 @@ from edsl import Model
5
5
  from edsl.language_models import LanguageModel
6
6
  from edsl.Base import Base
7
7
  from edsl.utilities.decorators import add_edsl_version, remove_edsl_version
8
- from edsl.utilities import is_valid_variable_name
8
+ from edsl.utilities.utilities import is_valid_variable_name
9
9
  from edsl.utilities.utilities import dict_hash
10
10
 
11
11
 
@@ -1,18 +1,13 @@
1
1
  import json
2
2
  import asyncio
3
3
  import warnings
4
- from rich import print
5
- from rich.console import Console
6
- from rich.syntax import Syntax
7
-
8
- from edsl.utilities.utilities import clean_json
9
-
10
- from edsl.utilities.repair_functions import extract_json_from_string
11
4
 
12
5
 
13
6
  async def async_repair(
14
7
  bad_json, error_message="", user_prompt=None, system_prompt=None, cache=None
15
8
  ):
9
+ from edsl.utilities.utilities import clean_json
10
+
16
11
  s = clean_json(bad_json)
17
12
 
18
13
  try:
@@ -27,6 +22,8 @@ async def async_repair(
27
22
  return valid_dict, success
28
23
 
29
24
  try:
25
+ from edsl.utilities.repair_functions import extract_json_from_string
26
+
30
27
  valid_dict = extract_json_from_string(s)
31
28
  success = True
32
29
  except ValueError:
@@ -98,6 +95,10 @@ async def async_repair(
98
95
  except json.JSONDecodeError:
99
96
  valid_dict = {}
100
97
  success = False
98
+ from rich import print
99
+ from rich.console import Console
100
+ from rich.syntax import Syntax
101
+
101
102
  console = Console()
102
103
  error_message = (
103
104
  f"All repairs. failed. LLM Model given [red]{str(bad_json)}[/red]"
@@ -1,15 +1,11 @@
1
1
  """A Notebook is a utility class that allows you to easily share/pull ipynbs from Coop."""
2
2
 
3
+ from __future__ import annotations
3
4
  import json
4
- import nbformat
5
- from nbconvert import HTMLExporter
6
5
  from typing import Dict, List, Optional
7
- from rich.table import Table
6
+ from uuid import uuid4
8
7
  from edsl.Base import Base
9
- from edsl.utilities.decorators import (
10
- add_edsl_version,
11
- remove_edsl_version,
12
- )
8
+ from edsl.utilities.decorators import add_edsl_version, remove_edsl_version
13
9
 
14
10
 
15
11
  class Notebook(Base):
@@ -34,6 +30,8 @@ class Notebook(Base):
34
30
  If no path is provided, assume this code is run in a notebook and try to load the current notebook from file.
35
31
  :param name: A name for the Notebook.
36
32
  """
33
+ import nbformat
34
+
37
35
  # Load current notebook path as fallback (VS Code only)
38
36
  path = path or globals().get("__vsc_ipynb_file__")
39
37
  if data is not None:
@@ -56,6 +54,37 @@ class Notebook(Base):
56
54
 
57
55
  self.name = name or self.default_name
58
56
 
57
+ @classmethod
58
+ def from_script(cls, path: str, name: Optional[str] = None) -> "Notebook":
59
+ # Read the script file
60
+ with open(path, "r") as script_file:
61
+ script_content = script_file.read()
62
+
63
+ # Create a new Jupyter notebook
64
+ nb = nbformat.v4.new_notebook()
65
+
66
+ # Add the script content to the first cell
67
+ first_cell = nbformat.v4.new_code_cell(script_content)
68
+ nb.cells.append(first_cell)
69
+
70
+ # Create a Notebook instance with the notebook data
71
+ notebook_instance = cls(nb)
72
+
73
+ return notebook_instance
74
+
75
+ @classmethod
76
+ def from_current_script(cls) -> "Notebook":
77
+ import inspect
78
+ import os
79
+
80
+ # Get the path to the current file
81
+ current_frame = inspect.currentframe()
82
+ caller_frame = inspect.getouterframes(current_frame, 2)
83
+ current_file_path = os.path.abspath(caller_frame[1].filename)
84
+
85
+ # Use from_script to create the notebook
86
+ return cls.from_script(current_file_path)
87
+
59
88
  def __eq__(self, other):
60
89
  """
61
90
  Check if two Notebooks are equal.
@@ -103,6 +132,9 @@ class Notebook(Base):
103
132
  """
104
133
  Return HTML representation of Notebook.
105
134
  """
135
+ from nbconvert import HTMLExporter
136
+ import nbformat
137
+
106
138
  notebook = nbformat.from_dict(self.data)
107
139
  html_exporter = HTMLExporter(template_name="basic")
108
140
  (body, _) = html_exporter.from_notebook_node(notebook)
@@ -143,6 +175,8 @@ class Notebook(Base):
143
175
  """
144
176
  Display a Notebook as a rich table.
145
177
  """
178
+ from rich.table import Table
179
+
146
180
  table_data, column_names = self._table()
147
181
  table = Table(title=f"{self.__class__.__name__} Attributes")
148
182
  for column in column_names:
@@ -155,10 +189,13 @@ class Notebook(Base):
155
189
  return table
156
190
 
157
191
  @classmethod
158
- def example(cls) -> "Notebook":
192
+ def example(cls, randomize: bool = False) -> Notebook:
159
193
  """
160
- Return an example Notebook.
194
+ Returns an example Notebook instance.
195
+
196
+ :param randomize: If True, adds a random string one of the cells' output.
161
197
  """
198
+ addition = "" if not randomize else str(uuid4())
162
199
  cells = [
163
200
  {
164
201
  "cell_type": "markdown",
@@ -173,7 +210,7 @@ class Notebook(Base):
173
210
  {
174
211
  "name": "stdout",
175
212
  "output_type": "stream",
176
- "text": "Hello world!\n",
213
+ "text": f"Hello world!\n{addition}",
177
214
  }
178
215
  ],
179
216
  "source": 'print("Hello world!")',
edsl/prompts/Prompt.py CHANGED
@@ -1,12 +1,16 @@
1
- """Class for creating prompts to be used in a survey."""
2
-
3
1
  from __future__ import annotations
4
2
  from typing import Optional
5
3
  from abc import ABC
6
4
  from typing import Any, List
7
5
 
8
6
  from rich.table import Table
9
- from jinja2 import Template, Environment, meta, TemplateSyntaxError
7
+ from jinja2 import Template, Environment, meta, TemplateSyntaxError, Undefined
8
+
9
+
10
+ class PreserveUndefined(Undefined):
11
+ def __str__(self):
12
+ return "{{ " + self._undefined_name + " }}"
13
+
10
14
 
11
15
  from edsl.exceptions.prompts import TemplateRenderError
12
16
  from edsl.prompts.prompt_config import (
@@ -35,6 +39,10 @@ class PromptBase(
35
39
 
36
40
  return data_to_html(self.to_dict())
37
41
 
42
+ def __len__(self):
43
+ """Return the length of the prompt text."""
44
+ return len(self.text)
45
+
38
46
  @classmethod
39
47
  def prompt_attributes(cls) -> List[str]:
40
48
  """Return the prompt class attributes."""
@@ -75,10 +83,10 @@ class PromptBase(
75
83
  >>> p = Prompt("Hello, {{person}}")
76
84
  >>> p2 = Prompt("How are you?")
77
85
  >>> p + p2
78
- Prompt(text='Hello, {{person}}How are you?')
86
+ Prompt(text=\"""Hello, {{person}}How are you?\""")
79
87
 
80
88
  >>> p + "How are you?"
81
- Prompt(text='Hello, {{person}}How are you?')
89
+ Prompt(text=\"""Hello, {{person}}How are you?\""")
82
90
  """
83
91
  if isinstance(other_prompt, str):
84
92
  return self.__class__(self.text + other_prompt)
@@ -114,7 +122,7 @@ class PromptBase(
114
122
  Example:
115
123
  >>> p = Prompt("Hello, {{person}}")
116
124
  >>> p
117
- Prompt(text='Hello, {{person}}')
125
+ Prompt(text=\"""Hello, {{person}}\""")
118
126
  """
119
127
  return f'Prompt(text="""{self.text}""")'
120
128
 
@@ -137,7 +145,7 @@ class PromptBase(
137
145
  :param template: The template to find the variables in.
138
146
 
139
147
  """
140
- env = Environment()
148
+ env = Environment(undefined=PreserveUndefined)
141
149
  ast = env.parse(template)
142
150
  return list(meta.find_undeclared_variables(ast))
143
151
 
@@ -186,13 +194,16 @@ class PromptBase(
186
194
 
187
195
  >>> p = Prompt("Hello, {{person}}")
188
196
  >>> p.render({"person": "John"})
189
- 'Hello, John'
197
+ Prompt(text=\"""Hello, John\""")
190
198
 
191
199
  >>> p.render({"person": "Mr. {{last_name}}", "last_name": "Horton"})
192
- 'Hello, Mr. Horton'
200
+ Prompt(text=\"""Hello, Mr. Horton\""")
193
201
 
194
202
  >>> p.render({"person": "Mr. {{last_name}}", "last_name": "Ho{{letter}}ton"}, max_nesting = 1)
195
- 'Hello, Mr. Horton'
203
+ Prompt(text=\"""Hello, Mr. Ho{{ letter }}ton\""")
204
+
205
+ >>> p.render({"person": "Mr. {{last_name}}"})
206
+ Prompt(text=\"""Hello, Mr. {{ last_name }}\""")
196
207
  """
197
208
  new_text = self._render(
198
209
  self.text, primary_replacement, **additional_replacements
@@ -216,12 +227,13 @@ class PromptBase(
216
227
  >>> codebook = {"age": "Age"}
217
228
  >>> p = Prompt("You are an agent named {{ name }}. {{ codebook['age']}}: {{ age }}")
218
229
  >>> p.render({"name": "John", "age": 44}, codebook=codebook)
219
- 'You are an agent named John. Age: 44'
230
+ Prompt(text=\"""You are an agent named John. Age: 44\""")
220
231
  """
232
+ env = Environment(undefined=PreserveUndefined)
221
233
  try:
222
234
  previous_text = None
223
235
  for _ in range(MAX_NESTING):
224
- rendered_text = Template(text).render(
236
+ rendered_text = env.from_string(text).render(
225
237
  primary_replacement, **additional_replacements
226
238
  )
227
239
  if rendered_text == previous_text:
@@ -258,7 +270,7 @@ class PromptBase(
258
270
  >>> p = Prompt("Hello, {{person}}")
259
271
  >>> p2 = Prompt.from_dict(p.to_dict())
260
272
  >>> p2
261
- Prompt(text='Hello, {{person}}')
273
+ Prompt(text=\"""Hello, {{person}}\""")
262
274
 
263
275
  """
264
276
  class_name = data["class_name"]
@@ -290,6 +302,12 @@ class Prompt(PromptBase):
290
302
  component_type = ComponentTypes.GENERIC
291
303
 
292
304
 
305
+ if __name__ == "__main__":
306
+ print("Running doctests...")
307
+ import doctest
308
+
309
+ doctest.testmod()
310
+
293
311
  from edsl.prompts.library.question_multiple_choice import *
294
312
  from edsl.prompts.library.agent_instructions import *
295
313
  from edsl.prompts.library.agent_persona import *
@@ -302,9 +320,3 @@ from edsl.prompts.library.question_numerical import *
302
320
  from edsl.prompts.library.question_rank import *
303
321
  from edsl.prompts.library.question_extract import *
304
322
  from edsl.prompts.library.question_list import *
305
-
306
-
307
- if __name__ == "__main__":
308
- import doctest
309
-
310
- doctest.testmod()
@@ -1,8 +1,8 @@
1
1
  """This module contains the Question class, which is the base class for all questions in EDSL."""
2
2
 
3
3
  from __future__ import annotations
4
+ import time
4
5
  from abc import ABC, abstractmethod
5
- from rich.table import Table
6
6
  from typing import Any, Type, Optional, List, Callable
7
7
  import copy
8
8
 
@@ -12,7 +12,7 @@ from edsl.exceptions import (
12
12
  )
13
13
  from edsl.questions.descriptors import QuestionNameDescriptor, QuestionTextDescriptor
14
14
 
15
- from edsl.prompts.registry import get_classes as prompt_lookup
15
+
16
16
  from edsl.questions.AnswerValidatorMixin import AnswerValidatorMixin
17
17
  from edsl.questions.RegisterQuestionsMeta import RegisterQuestionsMeta
18
18
  from edsl.Base import PersistenceMixin, RichPrintingMixin
@@ -124,6 +124,8 @@ class QuestionBase(
124
124
  :param model: The language model to use. If None, assumes does not matter.
125
125
 
126
126
  """
127
+ from edsl.prompts.registry import get_classes as prompt_lookup
128
+
127
129
  applicable_prompts = prompt_lookup(
128
130
  component_type="question_instructions",
129
131
  question_type=cls.question_type,
@@ -173,15 +175,16 @@ class QuestionBase(
173
175
  def add_model_instructions(
174
176
  self, *, instructions: str, model: Optional[str] = None
175
177
  ) -> None:
176
- """Add model-specific instructions for the question.
178
+ """Add model-specific instructions for the question that override the default instructions.
177
179
 
178
180
  :param instructions: The instructions to add. This is typically a jinja2 template.
179
181
  :param model: The language model for this instruction.
180
182
 
181
183
  >>> from edsl.questions import QuestionFreeText
182
184
  >>> q = QuestionFreeText(question_name = "color", question_text = "What is your favorite color?")
183
- >>> q.add_model_instructions(instructions = "Answer in valid JSON like so {'answer': 'comment: <>}", model = "gpt3")
184
-
185
+ >>> q.add_model_instructions(instructions = "{{question_text}}. Answer in valid JSON like so {'answer': 'comment: <>}", model = "gpt3")
186
+ >>> q.get_instructions(model = "gpt3")
187
+ Prompt(text=\"""{{question_text}}. Answer in valid JSON like so {'answer': 'comment: <>}\""")
185
188
  """
186
189
  from edsl import Model
187
190
 
@@ -201,6 +204,13 @@ class QuestionBase(
201
204
  """Get the mathcing question-answering instructions for the question.
202
205
 
203
206
  :param model: The language model to use.
207
+
208
+ >>> from edsl import QuestionFreeText
209
+ >>> QuestionFreeText.example().get_instructions()
210
+ Prompt(text=\"""You are being asked the following question: {{question_text}}
211
+ Return a valid JSON formatted like this:
212
+ {"answer": "<put free text answer here>"}
213
+ \""")
204
214
  """
205
215
  from edsl.prompts.Prompt import Prompt
206
216
 
@@ -293,7 +303,16 @@ class QuestionBase(
293
303
  print_json(json.dumps(self.to_dict()))
294
304
 
295
305
  def __call__(self, just_answer=True, model=None, agent=None, **kwargs):
296
- """Call the question."""
306
+ """Call the question.
307
+
308
+ >>> from edsl.language_models import LanguageModel
309
+ >>> m = LanguageModel.example(canned_response = "Yo, what's up?", test_model = True)
310
+ >>> from edsl import QuestionFreeText
311
+ >>> q = QuestionFreeText(question_name = "color", question_text = "What is your favorite color?")
312
+ >>> q(model = m)
313
+ "Yo, what's up?"
314
+
315
+ """
297
316
  survey = self.to_survey()
298
317
  results = survey(model=model, agent=agent, **kwargs)
299
318
  if just_answer:
@@ -304,7 +323,6 @@ class QuestionBase(
304
323
  async def run_async(self, just_answer=True, model=None, agent=None, **kwargs):
305
324
  """Call the question."""
306
325
  survey = self.to_survey()
307
- ## asyncio.run(survey.async_call());
308
326
  results = await survey.run_async(model=model, agent=agent, **kwargs)
309
327
  if just_answer:
310
328
  return results.select(f"answer.{self.question_name}").first()
@@ -383,29 +401,34 @@ class QuestionBase(
383
401
  s = Survey([self, other])
384
402
  return s
385
403
 
386
- def to_survey(self):
404
+ def to_survey(self) -> "Survey":
387
405
  """Turn a single question into a survey."""
388
406
  from edsl.surveys.Survey import Survey
389
407
 
390
408
  s = Survey([self])
391
409
  return s
392
410
 
393
- def run(self, *args, **kwargs):
411
+ def run(self, *args, **kwargs) -> "Results":
394
412
  """Turn a single question into a survey and run it."""
395
413
  from edsl.surveys.Survey import Survey
396
414
 
397
415
  s = self.to_survey()
398
416
  return s.run(*args, **kwargs)
399
417
 
400
- def by(self, *args):
401
- """Turn a single question into a survey and run it."""
418
+ def by(self, *args) -> "Jobs":
419
+ """Turn a single question into a survey and then a Job."""
402
420
  from edsl.surveys.Survey import Survey
403
421
 
404
422
  s = Survey([self])
405
423
  return s.by(*args)
406
424
 
407
- def human_readable(self):
408
- """Print the question in a human readable format."""
425
+ def human_readable(self) -> str:
426
+ """Print the question in a human readable format.
427
+
428
+ >>> from edsl.questions import QuestionFreeText
429
+ >>> QuestionFreeText.example().human_readable()
430
+ 'Question Type: free_text\\nQuestion: How are you?'
431
+ """
409
432
  lines = []
410
433
  lines.append(f"Question Type: {self.question_type}")
411
434
  lines.append(f"Question: {self.question_text}")
@@ -475,6 +498,8 @@ class QuestionBase(
475
498
 
476
499
  def rich_print(self):
477
500
  """Print the question in a rich format."""
501
+ from rich.table import Table
502
+
478
503
  table = Table(show_header=True, header_style="bold magenta")
479
504
  table.add_column("Question Name", style="dim")
480
505
  table.add_column("Question Type")
@@ -1,11 +1,8 @@
1
1
  from __future__ import annotations
2
2
  import random
3
- import textwrap
4
3
  from typing import Any, Optional, Union
5
4
  from edsl.questions.QuestionBase import QuestionBase
6
5
  from edsl.questions.descriptors import IntegerDescriptor, QuestionOptionsDescriptor
7
- from edsl.scenarios import Scenario
8
- from edsl.utilities import random_string
9
6
 
10
7
 
11
8
  class QuestionBudget(QuestionBase):
@@ -46,7 +43,7 @@ class QuestionBudget(QuestionBase):
46
43
  return answer
47
44
 
48
45
  def _translate_answer_code_to_answer(
49
- self, answer_codes: dict[str, int], scenario: Scenario = None
46
+ self, answer_codes: dict[str, int], scenario: "Scenario" = None
50
47
  ):
51
48
  """
52
49
  Translate the answer codes to the actual answers.
@@ -63,6 +60,8 @@ class QuestionBudget(QuestionBase):
63
60
 
64
61
  def _simulate_answer(self, human_readable=True):
65
62
  """Simulate a valid answer for debugging purposes (what the validator expects)."""
63
+ from edsl.utilities.utilities import random_string
64
+
66
65
  if human_readable:
67
66
  keys = self.question_options
68
67
  else:
@@ -163,8 +162,8 @@ def main():
163
162
 
164
163
 
165
164
  if __name__ == "__main__":
166
- q = QuestionBudget.example()
167
- results = q.run()
165
+ # q = QuestionBudget.example()
166
+ # results = q.run()
168
167
 
169
168
  import doctest
170
169
 
@@ -9,8 +9,6 @@ from edsl.questions.descriptors import (
9
9
  IntegerDescriptor,
10
10
  QuestionOptionsDescriptor,
11
11
  )
12
- from edsl.scenarios import Scenario
13
- from edsl.utilities import random_string
14
12
 
15
13
 
16
14
  class QuestionCheckBox(QuestionBase):
@@ -55,13 +53,17 @@ class QuestionCheckBox(QuestionBase):
55
53
  self._validate_answer_checkbox(answer)
56
54
  return answer
57
55
 
58
- def _translate_answer_code_to_answer(self, answer_codes, scenario: Scenario = None):
56
+ def _translate_answer_code_to_answer(
57
+ self, answer_codes, scenario: "Scenario" = None
58
+ ):
59
59
  """
60
60
  Translate the answer code to the actual answer.
61
61
 
62
62
  For example, for question options ["a", "b", "c"],the answer codes are 0, 1, and 2.
63
63
  The LLM will respond with [0,1] and this code will translate it to ["a","b"].
64
64
  """
65
+ from edsl.scenarios.Scenario import Scenario
66
+
65
67
  scenario = scenario or Scenario()
66
68
  translated_options = [
67
69
  Template(option).render(scenario) for option in self.question_options
@@ -73,6 +75,8 @@ class QuestionCheckBox(QuestionBase):
73
75
 
74
76
  def _simulate_answer(self, human_readable=True) -> dict[str, Union[int, str]]:
75
77
  """Simulate a valid answer for debugging purposes."""
78
+ from edsl.utilities.utilities import random_string
79
+
76
80
  min_selections = self.min_selections or 1
77
81
  max_selections = self.max_selections or len(self.question_options)
78
82
  num_selections = random.randint(min_selections, max_selections)
@@ -2,8 +2,6 @@ from __future__ import annotations
2
2
  from typing import Any
3
3
  from edsl.questions.QuestionBase import QuestionBase
4
4
  from edsl.questions.descriptors import AnswerTemplateDescriptor
5
- from edsl.scenarios import Scenario
6
- from edsl.utilities import random_string
7
5
 
8
6
 
9
7
  class QuestionExtract(QuestionBase):
@@ -44,12 +42,14 @@ class QuestionExtract(QuestionBase):
44
42
  self._validate_answer_extract(answer)
45
43
  return answer
46
44
 
47
- def _translate_answer_code_to_answer(self, answer, scenario: Scenario = None):
45
+ def _translate_answer_code_to_answer(self, answer, scenario: "Scenario" = None):
48
46
  """Return the answer in a human-readable format."""
49
47
  return answer
50
48
 
51
49
  def _simulate_answer(self, human_readable: bool = True) -> dict[str, str]:
52
50
  """Simulate a valid answer for debugging purposes."""
51
+ from edsl.utilities.utilities import random_string
52
+
53
53
  return {
54
54
  "answer": {key: random_string() for key in self.answer_template.keys()},
55
55
  "comment": random_string(),
@@ -106,6 +106,8 @@ def main():
106
106
  q.to_dict()
107
107
  assert q.from_dict(q.to_dict()) == q
108
108
 
109
+
110
+ if __name__ == "__main__":
109
111
  import doctest
110
112
 
111
113
  doctest.testmod(optionflags=doctest.ELLIPSIS)
@@ -1,9 +1,8 @@
1
1
  from __future__ import annotations
2
2
  import textwrap
3
3
  from typing import Any, Optional
4
+ from uuid import uuid4
4
5
  from edsl.questions.QuestionBase import QuestionBase
5
- from edsl.scenarios import Scenario
6
- from edsl.utilities import random_string
7
6
 
8
7
 
9
8
  class QuestionFreeText(QuestionBase):
@@ -43,12 +42,14 @@ class QuestionFreeText(QuestionBase):
43
42
  self._validate_answer_key_value(answer, "answer", str)
44
43
  return answer
45
44
 
46
- def _translate_answer_code_to_answer(self, answer, scenario: Scenario = None):
45
+ def _translate_answer_code_to_answer(self, answer, scenario: "Scenario" = None):
47
46
  """Do nothing, because the answer is already in a human-readable format."""
48
47
  return answer
49
48
 
50
49
  def _simulate_answer(self, human_readable: bool = True) -> dict[str, str]:
51
50
  """Simulate a valid answer for debugging purposes."""
51
+ from edsl.utilities.utilities import random_string
52
+
52
53
  return {"answer": random_string()}
53
54
 
54
55
  @property
@@ -65,9 +66,10 @@ class QuestionFreeText(QuestionBase):
65
66
  return question_html_content
66
67
 
67
68
  @classmethod
68
- def example(cls) -> QuestionFreeText:
69
+ def example(cls, randomize: bool = False) -> QuestionFreeText:
69
70
  """Return an example instance of a free text question."""
70
- return cls(question_name="how_are_you", question_text="How are you?")
71
+ addition = "" if not randomize else str(uuid4())
72
+ return cls(question_name="how_are_you", question_text=f"How are you?{addition}")
71
73
 
72
74
 
73
75
  def main():