edsl 0.1.30.dev4__py3-none-any.whl → 0.1.31__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 (47) hide show
  1. edsl/__version__.py +1 -1
  2. edsl/agents/Invigilator.py +7 -2
  3. edsl/agents/PromptConstructionMixin.py +18 -1
  4. edsl/config.py +4 -0
  5. edsl/conjure/Conjure.py +6 -0
  6. edsl/coop/coop.py +4 -0
  7. edsl/coop/utils.py +9 -1
  8. edsl/data/CacheHandler.py +3 -4
  9. edsl/enums.py +2 -0
  10. edsl/inference_services/DeepInfraService.py +6 -91
  11. edsl/inference_services/GroqService.py +18 -0
  12. edsl/inference_services/InferenceServicesCollection.py +13 -5
  13. edsl/inference_services/OpenAIService.py +64 -21
  14. edsl/inference_services/registry.py +2 -1
  15. edsl/jobs/Jobs.py +80 -33
  16. edsl/jobs/buckets/TokenBucket.py +24 -5
  17. edsl/jobs/interviews/Interview.py +122 -75
  18. edsl/jobs/interviews/InterviewExceptionEntry.py +101 -0
  19. edsl/jobs/interviews/InterviewTaskBuildingMixin.py +58 -52
  20. edsl/jobs/interviews/interview_exception_tracking.py +68 -10
  21. edsl/jobs/runners/JobsRunnerAsyncio.py +112 -81
  22. edsl/jobs/runners/JobsRunnerStatusData.py +0 -237
  23. edsl/jobs/runners/JobsRunnerStatusMixin.py +291 -35
  24. edsl/jobs/tasks/QuestionTaskCreator.py +1 -5
  25. edsl/jobs/tasks/TaskCreators.py +8 -2
  26. edsl/jobs/tasks/TaskHistory.py +145 -1
  27. edsl/language_models/LanguageModel.py +135 -75
  28. edsl/language_models/ModelList.py +8 -2
  29. edsl/language_models/registry.py +16 -0
  30. edsl/questions/QuestionFunctional.py +34 -2
  31. edsl/questions/QuestionMultipleChoice.py +58 -8
  32. edsl/questions/QuestionNumerical.py +0 -1
  33. edsl/questions/descriptors.py +42 -2
  34. edsl/results/DatasetExportMixin.py +258 -75
  35. edsl/results/Result.py +53 -5
  36. edsl/results/Results.py +66 -27
  37. edsl/results/ResultsToolsMixin.py +1 -1
  38. edsl/scenarios/Scenario.py +14 -0
  39. edsl/scenarios/ScenarioList.py +59 -21
  40. edsl/scenarios/ScenarioListExportMixin.py +16 -5
  41. edsl/scenarios/ScenarioListPdfMixin.py +3 -0
  42. edsl/study/Study.py +2 -2
  43. edsl/surveys/Survey.py +35 -1
  44. {edsl-0.1.30.dev4.dist-info → edsl-0.1.31.dist-info}/METADATA +4 -2
  45. {edsl-0.1.30.dev4.dist-info → edsl-0.1.31.dist-info}/RECORD +47 -45
  46. {edsl-0.1.30.dev4.dist-info → edsl-0.1.31.dist-info}/WHEEL +1 -1
  47. {edsl-0.1.30.dev4.dist-info → edsl-0.1.31.dist-info}/LICENSE +0 -0
@@ -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)
@@ -7,9 +7,37 @@ import asyncio
7
7
  import json
8
8
  import time
9
9
  import os
10
+ import hashlib
10
11
  from typing import Coroutine, Any, Callable, Type, List, get_type_hints
11
12
  from abc import ABC, abstractmethod
12
13
 
14
+
15
+ class IntendedModelCallOutcome:
16
+ "This is a tuple-like class that holds the response, cache_used, and cache_key."
17
+
18
+ def __init__(self, response: dict, cache_used: bool, cache_key: str):
19
+ self.response = response
20
+ self.cache_used = cache_used
21
+ self.cache_key = cache_key
22
+
23
+ def __iter__(self):
24
+ """Iterate over the class attributes.
25
+
26
+ >>> a, b, c = IntendedModelCallOutcome({'answer': "yes"}, True, 'x1289')
27
+ >>> a
28
+ {'answer': 'yes'}
29
+ """
30
+ yield self.response
31
+ yield self.cache_used
32
+ yield self.cache_key
33
+
34
+ def __len__(self):
35
+ return 3
36
+
37
+ def __repr__(self):
38
+ return f"IntendedModelCallOutcome(response = {self.response}, cache_used = {self.cache_used}, cache_key = '{self.cache_key}')"
39
+
40
+
13
41
  from edsl.config import CONFIG
14
42
 
15
43
  from edsl.utilities.decorators import sync_wrapper, jupyter_nb_handler
@@ -96,6 +124,11 @@ class LanguageModel(
96
124
  # Skip the API key check. Sometimes this is useful for testing.
97
125
  self._api_token = None
98
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
+
99
132
  @property
100
133
  def api_token(self) -> str:
101
134
  if not hasattr(self, "_api_token"):
@@ -149,7 +182,7 @@ class LanguageModel(
149
182
  key_value = os.getenv(key_name)
150
183
  return key_value is not None
151
184
 
152
- def __hash__(self):
185
+ def __hash__(self) -> str:
153
186
  """Allow the model to be used as a key in a dictionary."""
154
187
  from edsl.utilities.utilities import dict_hash
155
188
 
@@ -216,19 +249,25 @@ class LanguageModel(
216
249
  >>> LanguageModel._overide_default_parameters(passed_parameter_dict={"temperature": 0.5}, default_parameter_dict={"temperature":0.9, "max_tokens": 1000})
217
250
  {'temperature': 0.5, 'max_tokens': 1000}
218
251
  """
219
- parameters = dict({})
220
- for parameter, default_value in default_parameter_dict.items():
221
- if parameter in passed_parameter_dict:
222
- parameters[parameter] = passed_parameter_dict[parameter]
223
- else:
224
- parameters[parameter] = default_value
225
- return parameters
252
+ # parameters = dict({})
253
+
254
+ return {
255
+ parameter_name: passed_parameter_dict.get(parameter_name, default_value)
256
+ for parameter_name, default_value in default_parameter_dict.items()
257
+ }
258
+
259
+ def __call__(self, user_prompt: str, system_prompt: str):
260
+ return self.execute_model_call(user_prompt, system_prompt)
226
261
 
227
262
  @abstractmethod
228
263
  async def async_execute_model_call(user_prompt: str, system_prompt: str):
229
- """Execute the model call and returns the result as a coroutine.
264
+ """Execute the model call and returns a coroutine.
230
265
 
231
266
  >>> m = LanguageModel.example(test_model = True)
267
+ >>> async def test(): return await m.async_execute_model_call("Hello, model!", "You are a helpful agent.")
268
+ >>> asyncio.run(test())
269
+ {'message': '{"answer": "Hello world"}'}
270
+
232
271
  >>> m.execute_model_call("Hello, model!", "You are a helpful agent.")
233
272
  {'message': '{"answer": "Hello world"}'}
234
273
 
@@ -274,11 +313,38 @@ class LanguageModel(
274
313
 
275
314
  What is returned by the API is model-specific and often includes meta-data that we do not need.
276
315
  For example, here is the results from a call to GPT-4:
277
- To actually tract the response, we need to grab
316
+ To actually track the response, we need to grab
278
317
  data["choices[0]"]["message"]["content"].
279
318
  """
280
319
  raise NotImplementedError
281
320
 
321
+ async def _async_prepare_response(
322
+ self, model_call_outcome: IntendedModelCallOutcome, cache: "Cache"
323
+ ) -> dict:
324
+ """Prepare the response for return."""
325
+
326
+ model_response = {
327
+ "cache_used": model_call_outcome.cache_used,
328
+ "cache_key": model_call_outcome.cache_key,
329
+ "usage": model_call_outcome.response.get("usage", {}),
330
+ "raw_model_response": model_call_outcome.response,
331
+ }
332
+
333
+ answer_portion = self.parse_response(model_call_outcome.response)
334
+ try:
335
+ answer_dict = json.loads(answer_portion)
336
+ except json.JSONDecodeError as e:
337
+ # TODO: Turn into logs to generate issues
338
+ answer_dict, success = await repair(
339
+ bad_json=answer_portion, error_message=str(e), cache=cache
340
+ )
341
+ if not success:
342
+ raise Exception(
343
+ f"""Even the repair failed. The error was: {e}. The response was: {answer_portion}."""
344
+ )
345
+
346
+ return {**model_response, **answer_dict}
347
+
282
348
  async def async_get_raw_response(
283
349
  self,
284
350
  user_prompt: str,
@@ -286,7 +352,28 @@ class LanguageModel(
286
352
  cache: "Cache",
287
353
  iteration: int = 0,
288
354
  encoded_image=None,
289
- ) -> tuple[dict, bool, str]:
355
+ ) -> IntendedModelCallOutcome:
356
+ import warnings
357
+
358
+ warnings.warn(
359
+ "This method is deprecated. Use async_get_intended_model_call_outcome."
360
+ )
361
+ return await self._async_get_intended_model_call_outcome(
362
+ user_prompt=user_prompt,
363
+ system_prompt=system_prompt,
364
+ cache=cache,
365
+ iteration=iteration,
366
+ encoded_image=encoded_image,
367
+ )
368
+
369
+ async def _async_get_intended_model_call_outcome(
370
+ self,
371
+ user_prompt: str,
372
+ system_prompt: str,
373
+ cache: "Cache",
374
+ iteration: int = 0,
375
+ encoded_image=None,
376
+ ) -> IntendedModelCallOutcome:
290
377
  """Handle caching of responses.
291
378
 
292
379
  :param user_prompt: The user's prompt.
@@ -304,52 +391,49 @@ class LanguageModel(
304
391
 
305
392
  >>> from edsl import Cache
306
393
  >>> m = LanguageModel.example(test_model = True)
307
- >>> m.get_raw_response(user_prompt = "Hello", system_prompt = "hello", cache = Cache())
308
- ({'message': '{"answer": "Hello world"}'}, False, '24ff6ac2bc2f1729f817f261e0792577')
394
+ >>> m._get_intended_model_call_outcome(user_prompt = "Hello", system_prompt = "hello", cache = Cache())
395
+ IntendedModelCallOutcome(response = {'message': '{"answer": "Hello world"}'}, cache_used = False, cache_key = '24ff6ac2bc2f1729f817f261e0792577')
309
396
  """
310
- start_time = time.time()
397
+
398
+ if encoded_image:
399
+ # the image has is appended to the user_prompt for hash-lookup purposes
400
+ image_hash = hashlib.md5(encoded_image.encode()).hexdigest()
311
401
 
312
402
  cache_call_params = {
313
403
  "model": str(self.model),
314
404
  "parameters": self.parameters,
315
405
  "system_prompt": system_prompt,
316
- "user_prompt": user_prompt,
406
+ "user_prompt": user_prompt + "" if not encoded_image else f" {image_hash}",
317
407
  "iteration": iteration,
318
408
  }
319
-
320
- if encoded_image:
321
- import hashlib
322
-
323
- image_hash = hashlib.md5(encoded_image.encode()).hexdigest()
324
- cache_call_params["user_prompt"] = f"{user_prompt} {image_hash}"
325
-
326
409
  cached_response, cache_key = cache.fetch(**cache_call_params)
327
- if cached_response:
410
+
411
+ if cache_used := cached_response is not None:
328
412
  response = json.loads(cached_response)
329
- cache_used = True
330
413
  else:
331
- remote_call = hasattr(self, "remote") and self.remote
332
414
  f = (
333
415
  self.remote_async_execute_model_call
334
- if remote_call
416
+ if hasattr(self, "remote") and self.remote
335
417
  else self.async_execute_model_call
336
418
  )
337
- params = {"user_prompt": user_prompt, "system_prompt": system_prompt}
338
- if encoded_image:
339
- params["encoded_image"] = encoded_image
419
+ params = {
420
+ "user_prompt": user_prompt,
421
+ "system_prompt": system_prompt,
422
+ **({"encoded_image": encoded_image} if encoded_image else {}),
423
+ }
340
424
  response = await f(**params)
341
425
  new_cache_key = cache.store(
342
- user_prompt=user_prompt,
343
- model=str(self.model),
344
- parameters=self.parameters,
345
- system_prompt=system_prompt,
346
- response=response,
347
- iteration=iteration,
348
- )
349
- assert new_cache_key == cache_key
350
- cache_used = False
426
+ **cache_call_params, response=response
427
+ ) # store the response in the cache
428
+ assert new_cache_key == cache_key # should be the same
429
+
430
+ return IntendedModelCallOutcome(
431
+ response=response, cache_used=cache_used, cache_key=cache_key
432
+ )
351
433
 
352
- return response, cache_used, cache_key
434
+ _get_intended_model_call_outcome = sync_wrapper(
435
+ _async_get_intended_model_call_outcome
436
+ )
353
437
 
354
438
  get_raw_response = sync_wrapper(async_get_raw_response)
355
439
 
@@ -370,7 +454,7 @@ class LanguageModel(
370
454
  self,
371
455
  user_prompt: str,
372
456
  system_prompt: str,
373
- cache: Cache,
457
+ cache: "Cache",
374
458
  iteration: int = 1,
375
459
  encoded_image=None,
376
460
  ) -> dict:
@@ -388,36 +472,10 @@ class LanguageModel(
388
472
  "system_prompt": system_prompt,
389
473
  "iteration": iteration,
390
474
  "cache": cache,
475
+ **({"encoded_image": encoded_image} if encoded_image else {}),
391
476
  }
392
- if encoded_image:
393
- params["encoded_image"] = encoded_image
394
-
395
- raw_response, cache_used, cache_key = await self.async_get_raw_response(
396
- **params
397
- )
398
- response = self.parse_response(raw_response)
399
-
400
- try:
401
- dict_response = json.loads(response)
402
- except json.JSONDecodeError as e:
403
- # TODO: Turn into logs to generate issues
404
- dict_response, success = await repair(
405
- bad_json=response, error_message=str(e), cache=cache
406
- )
407
- if not success:
408
- raise Exception(
409
- f"""Even the repair failed. The error was: {e}. The response was: {response}."""
410
- )
411
-
412
- dict_response.update(
413
- {
414
- "cache_used": cache_used,
415
- "cache_key": cache_key,
416
- "usage": raw_response.get("usage", {}),
417
- "raw_model_response": raw_response,
418
- }
419
- )
420
- return dict_response
477
+ model_call_outcome = await self._async_get_intended_model_call_outcome(**params)
478
+ return await self._async_prepare_response(model_call_outcome, cache=cache)
421
479
 
422
480
  get_response = sync_wrapper(async_get_response)
423
481
 
@@ -494,7 +552,12 @@ class LanguageModel(
494
552
  return table
495
553
 
496
554
  @classmethod
497
- def example(cls, test_model: bool = False, canned_response: str = "Hello world"):
555
+ def example(
556
+ cls,
557
+ test_model: bool = False,
558
+ canned_response: str = "Hello world",
559
+ throw_exception: bool = False,
560
+ ):
498
561
  """Return a default instance of the class.
499
562
 
500
563
  >>> from edsl.language_models import LanguageModel
@@ -519,6 +582,8 @@ class LanguageModel(
519
582
  ) -> dict[str, Any]:
520
583
  await asyncio.sleep(0.1)
521
584
  # return {"message": """{"answer": "Hello, world"}"""}
585
+ if throw_exception:
586
+ raise Exception("This is a test error")
522
587
  return {"message": f'{{"answer": "{canned_response}"}}'}
523
588
 
524
589
  def parse_response(self, raw_response: dict[str, Any]) -> str:
@@ -536,8 +601,3 @@ if __name__ == "__main__":
536
601
  import doctest
537
602
 
538
603
  doctest.testmod(optionflags=doctest.ELLIPSIS)
539
-
540
- # from edsl.language_models import LanguageModel
541
-
542
- # from edsl.language_models import LanguageModel
543
- # print(LanguageModel.example())
@@ -86,8 +86,14 @@ class ModelList(Base, UserList):
86
86
  pass
87
87
 
88
88
  @classmethod
89
- def example(cl):
90
- return ModelList([LanguageModel.example() for _ in range(3)])
89
+ def example(cls, randomize: bool = False) -> "ModelList":
90
+ """
91
+ Returns an example ModelList instance.
92
+
93
+ :param randomize: If True, uses Model's randomize method.
94
+ """
95
+
96
+ return cls([Model.example(randomize) for _ in range(3)])
91
97
 
92
98
 
93
99
  if __name__ == "__main__":
@@ -1,4 +1,5 @@
1
1
  import textwrap
2
+ from random import random
2
3
 
3
4
 
4
5
  def get_model_class(model_name, registry=None):
@@ -35,6 +36,10 @@ class Model(metaclass=Meta):
35
36
  from edsl.inference_services.registry import default
36
37
 
37
38
  registry = registry or default
39
+
40
+ if isinstance(model_name, int):
41
+ model_name = cls.available(name_only=True)[model_name]
42
+
38
43
  factory = registry.create_model_factory(model_name)
39
44
  return factory(*args, **kwargs)
40
45
 
@@ -92,6 +97,17 @@ class Model(metaclass=Meta):
92
97
  print("OK!")
93
98
  print("\n")
94
99
 
100
+ @classmethod
101
+ def example(cls, randomize: bool = False) -> "Model":
102
+ """
103
+ Returns an example Model instance.
104
+
105
+ :param randomize: If True, the temperature is set to a random decimal between 0 and 1.
106
+ """
107
+ temperature = 0.5 if not randomize else round(random(), 2)
108
+ model_name = cls.default_model
109
+ return cls(model_name, temperature=temperature)
110
+
95
111
 
96
112
  if __name__ == "__main__":
97
113
  import doctest
@@ -4,10 +4,34 @@ import inspect
4
4
  from edsl.questions.QuestionBase import QuestionBase
5
5
 
6
6
  from edsl.utilities.restricted_python import create_restricted_function
7
+ from edsl.utilities.decorators import add_edsl_version, remove_edsl_version
7
8
 
8
9
 
9
10
  class QuestionFunctional(QuestionBase):
10
- """A special type of question that is *not* answered by an LLM."""
11
+ """A special type of question that is *not* answered by an LLM.
12
+
13
+ >>> from edsl import Scenario, Agent
14
+
15
+ # Create an instance of QuestionFunctional with the new function
16
+ >>> question = QuestionFunctional.example()
17
+
18
+ # Activate and test the function
19
+ >>> question.activate()
20
+ >>> scenario = Scenario({"numbers": [1, 2, 3, 4, 5]})
21
+ >>> agent = Agent(traits={"multiplier": 10})
22
+ >>> results = question.by(scenario).by(agent).run()
23
+ >>> results.select("answer.*").to_list()[0] == 150
24
+ True
25
+
26
+ # Serialize the question to a dictionary
27
+
28
+ >>> from edsl.questions.QuestionBase import QuestionBase
29
+ >>> new_question = QuestionBase.from_dict(question.to_dict())
30
+ >>> results = new_question.by(scenario).by(agent).run()
31
+ >>> results.select("answer.*").to_list()[0] == 150
32
+ True
33
+
34
+ """
11
35
 
12
36
  question_type = "functional"
13
37
  default_instructions = ""
@@ -73,6 +97,7 @@ class QuestionFunctional(QuestionBase):
73
97
  """Required by Question, but not used by QuestionFunctional."""
74
98
  raise NotImplementedError
75
99
 
100
+ @add_edsl_version
76
101
  def to_dict(self):
77
102
  return {
78
103
  "question_name": self.question_name,
@@ -113,4 +138,11 @@ def main():
113
138
  scenario = Scenario({"numbers": [1, 2, 3, 4, 5]})
114
139
  agent = Agent(traits={"multiplier": 10})
115
140
  results = question.by(scenario).by(agent).run()
116
- print(results)
141
+ assert results.select("answer.*").to_list()[0] == 150
142
+
143
+
144
+ if __name__ == "__main__":
145
+ # main()
146
+ import doctest
147
+
148
+ doctest.testmod(optionflags=doctest.ELLIPSIS)