edsl 0.1.30.dev5__py3-none-any.whl → 0.1.31.dev1__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/coop/utils.py +9 -1
- edsl/jobs/buckets/TokenBucket.py +3 -3
- edsl/jobs/interviews/Interview.py +10 -10
- edsl/jobs/interviews/InterviewTaskBuildingMixin.py +9 -7
- edsl/jobs/tasks/QuestionTaskCreator.py +2 -3
- edsl/language_models/LanguageModel.py +6 -1
- edsl/language_models/ModelList.py +8 -2
- edsl/language_models/registry.py +12 -0
- edsl/questions/QuestionFunctional.py +8 -7
- edsl/questions/QuestionMultipleChoice.py +14 -12
- edsl/questions/descriptors.py +6 -4
- edsl/results/DatasetExportMixin.py +174 -76
- edsl/results/Result.py +13 -11
- edsl/results/Results.py +19 -16
- edsl/results/ResultsToolsMixin.py +1 -1
- edsl/scenarios/ScenarioList.py +44 -19
- edsl/scenarios/ScenarioListExportMixin.py +1 -1
- edsl/surveys/Survey.py +11 -8
- {edsl-0.1.30.dev5.dist-info → edsl-0.1.31.dev1.dist-info}/METADATA +2 -1
- {edsl-0.1.30.dev5.dist-info → edsl-0.1.31.dev1.dist-info}/RECORD +23 -23
- {edsl-0.1.30.dev5.dist-info → edsl-0.1.31.dev1.dist-info}/LICENSE +0 -0
- {edsl-0.1.30.dev5.dist-info → edsl-0.1.31.dev1.dist-info}/WHEEL +0 -0
edsl/__version__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "0.1.
|
1
|
+
__version__ = "0.1.31.dev1"
|
edsl/coop/utils.py
CHANGED
@@ -2,6 +2,7 @@ from edsl import (
|
|
2
2
|
Agent,
|
3
3
|
AgentList,
|
4
4
|
Cache,
|
5
|
+
ModelList,
|
5
6
|
Notebook,
|
6
7
|
Results,
|
7
8
|
Scenario,
|
@@ -9,6 +10,7 @@ from edsl import (
|
|
9
10
|
Survey,
|
10
11
|
Study,
|
11
12
|
)
|
13
|
+
from edsl.language_models import LanguageModel
|
12
14
|
from edsl.questions import QuestionBase
|
13
15
|
from typing import Literal, Optional, Type, Union
|
14
16
|
|
@@ -16,6 +18,8 @@ EDSLObject = Union[
|
|
16
18
|
Agent,
|
17
19
|
AgentList,
|
18
20
|
Cache,
|
21
|
+
LanguageModel,
|
22
|
+
ModelList,
|
19
23
|
Notebook,
|
20
24
|
Type[QuestionBase],
|
21
25
|
Results,
|
@@ -29,6 +33,8 @@ ObjectType = Literal[
|
|
29
33
|
"agent",
|
30
34
|
"agent_list",
|
31
35
|
"cache",
|
36
|
+
"model",
|
37
|
+
"model_list",
|
32
38
|
"notebook",
|
33
39
|
"question",
|
34
40
|
"results",
|
@@ -62,8 +68,10 @@ class ObjectRegistry:
|
|
62
68
|
{"object_type": "agent", "edsl_class": Agent},
|
63
69
|
{"object_type": "agent_list", "edsl_class": AgentList},
|
64
70
|
{"object_type": "cache", "edsl_class": Cache},
|
65
|
-
{"object_type": "
|
71
|
+
{"object_type": "model", "edsl_class": LanguageModel},
|
72
|
+
{"object_type": "model_list", "edsl_class": ModelList},
|
66
73
|
{"object_type": "notebook", "edsl_class": Notebook},
|
74
|
+
{"object_type": "question", "edsl_class": QuestionBase},
|
67
75
|
{"object_type": "results", "edsl_class": Results},
|
68
76
|
{"object_type": "scenario", "edsl_class": Scenario},
|
69
77
|
{"object_type": "scenario_list", "edsl_class": ScenarioList},
|
edsl/jobs/buckets/TokenBucket.py
CHANGED
@@ -30,7 +30,7 @@ class TokenBucket:
|
|
30
30
|
if self.turbo_mode:
|
31
31
|
pass
|
32
32
|
else:
|
33
|
-
#pass
|
33
|
+
# pass
|
34
34
|
self.turbo_mode = True
|
35
35
|
self.capacity = float("inf")
|
36
36
|
self.refill_rate = float("inf")
|
@@ -74,7 +74,7 @@ class TokenBucket:
|
|
74
74
|
|
75
75
|
def refill(self) -> None:
|
76
76
|
"""Refill the bucket with new tokens based on elapsed time.
|
77
|
-
|
77
|
+
|
78
78
|
|
79
79
|
|
80
80
|
>>> bucket = TokenBucket(bucket_name="test", bucket_type="test", capacity=10, refill_rate=1)
|
@@ -82,7 +82,7 @@ class TokenBucket:
|
|
82
82
|
>>> bucket.refill()
|
83
83
|
>>> bucket.tokens > 0
|
84
84
|
True
|
85
|
-
|
85
|
+
|
86
86
|
"""
|
87
87
|
now = time.monotonic()
|
88
88
|
elapsed = now - self.last_refill
|
@@ -22,9 +22,11 @@ from edsl.jobs.interviews.InterviewStatusMixin import InterviewStatusMixin
|
|
22
22
|
|
23
23
|
import asyncio
|
24
24
|
|
25
|
+
|
25
26
|
def run_async(coro):
|
26
27
|
return asyncio.run(coro)
|
27
28
|
|
29
|
+
|
28
30
|
class Interview(InterviewStatusMixin, InterviewTaskBuildingMixin):
|
29
31
|
"""
|
30
32
|
An 'interview' is one agent answering one survey, with one language model, for a given scenario.
|
@@ -41,7 +43,7 @@ class Interview(InterviewStatusMixin, InterviewTaskBuildingMixin):
|
|
41
43
|
debug: Optional[bool] = False,
|
42
44
|
iteration: int = 0,
|
43
45
|
cache: Optional["Cache"] = None,
|
44
|
-
sidecar_model: Optional[
|
46
|
+
sidecar_model: Optional["LanguageModel"] = None,
|
45
47
|
):
|
46
48
|
"""Initialize the Interview instance.
|
47
49
|
|
@@ -98,7 +100,7 @@ class Interview(InterviewStatusMixin, InterviewTaskBuildingMixin):
|
|
98
100
|
model_buckets: ModelBuckets = None,
|
99
101
|
debug: bool = False,
|
100
102
|
stop_on_exception: bool = False,
|
101
|
-
sidecar_model: Optional[
|
103
|
+
sidecar_model: Optional["LanguageModel"] = None,
|
102
104
|
) -> tuple["Answers", List[dict[str, Any]]]:
|
103
105
|
"""
|
104
106
|
Conduct an Interview asynchronously.
|
@@ -146,13 +148,11 @@ class Interview(InterviewStatusMixin, InterviewTaskBuildingMixin):
|
|
146
148
|
if model_buckets is None or hasattr(self.agent, "answer_question_directly"):
|
147
149
|
model_buckets = ModelBuckets.infinity_bucket()
|
148
150
|
|
149
|
-
|
150
151
|
## build the tasks using the InterviewTaskBuildingMixin
|
151
152
|
## This is the key part---it creates a task for each question,
|
152
153
|
## with dependencies on the questions that must be answered before this one can be answered.
|
153
154
|
self.tasks = self._build_question_tasks(
|
154
|
-
debug=debug,
|
155
|
-
model_buckets=model_buckets
|
155
|
+
debug=debug, model_buckets=model_buckets
|
156
156
|
)
|
157
157
|
|
158
158
|
## 'Invigilators' are used to administer the survey
|
@@ -195,7 +195,7 @@ class Interview(InterviewStatusMixin, InterviewTaskBuildingMixin):
|
|
195
195
|
|
196
196
|
def _record_exception(self, task, exception: Exception) -> None:
|
197
197
|
"""Record an exception in the Interview instance.
|
198
|
-
|
198
|
+
|
199
199
|
It records the exception in the Interview instance, with the task name and the exception entry.
|
200
200
|
|
201
201
|
>>> i = Interview.example()
|
@@ -235,14 +235,14 @@ class Interview(InterviewStatusMixin, InterviewTaskBuildingMixin):
|
|
235
235
|
"""Return a string representation of the Interview instance."""
|
236
236
|
return f"Interview(agent = {repr(self.agent)}, survey = {repr(self.survey)}, scenario = {repr(self.scenario)}, model = {repr(self.model)})"
|
237
237
|
|
238
|
-
def duplicate(self, iteration: int, cache:
|
238
|
+
def duplicate(self, iteration: int, cache: "Cache") -> Interview:
|
239
239
|
"""Duplicate the interview, but with a new iteration number and cache.
|
240
|
-
|
240
|
+
|
241
241
|
>>> i = Interview.example()
|
242
242
|
>>> i2 = i.duplicate(1, None)
|
243
243
|
>>> i.iteration + 1 == i2.iteration
|
244
244
|
True
|
245
|
-
|
245
|
+
|
246
246
|
"""
|
247
247
|
return Interview(
|
248
248
|
agent=self.agent,
|
@@ -270,7 +270,7 @@ class Interview(InterviewStatusMixin, InterviewTaskBuildingMixin):
|
|
270
270
|
scenario = Scenario.example()
|
271
271
|
model = LanguageModel.example()
|
272
272
|
if throw_exception:
|
273
|
-
model = LanguageModel.example(test_model
|
273
|
+
model = LanguageModel.example(test_model=True, throw_exception=True)
|
274
274
|
agent = Agent.example()
|
275
275
|
return Interview(agent=agent, survey=survey, scenario=scenario, model=model)
|
276
276
|
return Interview(agent=agent, survey=survey, scenario=scenario, model=model)
|
@@ -25,7 +25,7 @@ TIMEOUT = float(CONFIG.get("EDSL_API_TIMEOUT"))
|
|
25
25
|
class InterviewTaskBuildingMixin:
|
26
26
|
def _build_invigilators(
|
27
27
|
self, debug: bool
|
28
|
-
) -> Generator[
|
28
|
+
) -> Generator["InvigilatorBase", None, None]:
|
29
29
|
"""Create an invigilator for each question.
|
30
30
|
|
31
31
|
:param debug: whether to use debug mode, in which case `InvigilatorDebug` is used.
|
@@ -35,7 +35,7 @@ class InterviewTaskBuildingMixin:
|
|
35
35
|
for question in self.survey.questions:
|
36
36
|
yield self._get_invigilator(question=question, debug=debug)
|
37
37
|
|
38
|
-
def _get_invigilator(self, question:
|
38
|
+
def _get_invigilator(self, question: "QuestionBase", debug: bool) -> "Invigilator":
|
39
39
|
"""Return an invigilator for the given question.
|
40
40
|
|
41
41
|
:param question: the question to be answered
|
@@ -84,7 +84,7 @@ class InterviewTaskBuildingMixin:
|
|
84
84
|
return tuple(tasks) # , invigilators
|
85
85
|
|
86
86
|
def _get_tasks_that_must_be_completed_before(
|
87
|
-
self, *, tasks: list[asyncio.Task], question:
|
87
|
+
self, *, tasks: list[asyncio.Task], question: "QuestionBase"
|
88
88
|
) -> Generator[asyncio.Task, None, None]:
|
89
89
|
"""Return the tasks that must be completed before the given question can be answered.
|
90
90
|
|
@@ -100,7 +100,7 @@ class InterviewTaskBuildingMixin:
|
|
100
100
|
def _create_question_task(
|
101
101
|
self,
|
102
102
|
*,
|
103
|
-
question:
|
103
|
+
question: "QuestionBase",
|
104
104
|
tasks_that_must_be_completed_before: list[asyncio.Task],
|
105
105
|
model_buckets: ModelBuckets,
|
106
106
|
debug: bool,
|
@@ -179,8 +179,10 @@ class InterviewTaskBuildingMixin:
|
|
179
179
|
return AgentResponseDict(**response)
|
180
180
|
except Exception as e:
|
181
181
|
raise e
|
182
|
-
|
183
|
-
def _add_answer(
|
182
|
+
|
183
|
+
def _add_answer(
|
184
|
+
self, response: "AgentResponseDict", question: "QuestionBase"
|
185
|
+
) -> None:
|
184
186
|
"""Add the answer to the answers dictionary.
|
185
187
|
|
186
188
|
:param response: the response to the question.
|
@@ -188,7 +190,7 @@ class InterviewTaskBuildingMixin:
|
|
188
190
|
"""
|
189
191
|
self.answers.add_answer(response=response, question=question)
|
190
192
|
|
191
|
-
def _skip_this_question(self, current_question:
|
193
|
+
def _skip_this_question(self, current_question: "QuestionBase") -> bool:
|
192
194
|
"""Determine if the current question should be skipped.
|
193
195
|
|
194
196
|
:param current_question: the question to be answered.
|
@@ -88,8 +88,7 @@ class QuestionTaskCreator(UserList):
|
|
88
88
|
self.append(task)
|
89
89
|
|
90
90
|
def generate_task(self, debug: bool) -> asyncio.Task:
|
91
|
-
"""Create a task that depends on the passed-in dependencies.
|
92
|
-
"""
|
91
|
+
"""Create a task that depends on the passed-in dependencies."""
|
93
92
|
task = asyncio.create_task(
|
94
93
|
self._run_task_async(debug), name=self.question.question_name
|
95
94
|
)
|
@@ -145,7 +144,7 @@ class QuestionTaskCreator(UserList):
|
|
145
144
|
self.task_status = TaskStatus.FAILED
|
146
145
|
raise e
|
147
146
|
|
148
|
-
if results.get(
|
147
|
+
if results.get("cache_used", False):
|
149
148
|
self.tokens_bucket.add_tokens(requested_tokens)
|
150
149
|
self.requests_bucket.add_tokens(1)
|
151
150
|
self.from_cache = True
|
@@ -494,7 +494,12 @@ class LanguageModel(
|
|
494
494
|
return table
|
495
495
|
|
496
496
|
@classmethod
|
497
|
-
def example(
|
497
|
+
def example(
|
498
|
+
cls,
|
499
|
+
test_model: bool = False,
|
500
|
+
canned_response: str = "Hello world",
|
501
|
+
throw_exception: bool = False,
|
502
|
+
):
|
498
503
|
"""Return a default instance of the class.
|
499
504
|
|
500
505
|
>>> from edsl.language_models import LanguageModel
|
@@ -86,8 +86,14 @@ class ModelList(Base, UserList):
|
|
86
86
|
pass
|
87
87
|
|
88
88
|
@classmethod
|
89
|
-
def example(
|
90
|
-
|
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__":
|
edsl/language_models/registry.py
CHANGED
@@ -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):
|
@@ -92,6 +93,17 @@ class Model(metaclass=Meta):
|
|
92
93
|
print("OK!")
|
93
94
|
print("\n")
|
94
95
|
|
96
|
+
@classmethod
|
97
|
+
def example(cls, randomize: bool = False) -> "Model":
|
98
|
+
"""
|
99
|
+
Returns an example Model instance.
|
100
|
+
|
101
|
+
:param randomize: If True, the temperature is set to a random decimal between 0 and 1.
|
102
|
+
"""
|
103
|
+
temperature = 0.5 if not randomize else round(random(), 2)
|
104
|
+
model_name = cls.default_model
|
105
|
+
return cls(model_name, temperature=temperature)
|
106
|
+
|
95
107
|
|
96
108
|
if __name__ == "__main__":
|
97
109
|
import doctest
|
@@ -6,11 +6,12 @@ from edsl.questions.QuestionBase import QuestionBase
|
|
6
6
|
from edsl.utilities.restricted_python import create_restricted_function
|
7
7
|
from edsl.utilities.decorators import add_edsl_version, remove_edsl_version
|
8
8
|
|
9
|
+
|
9
10
|
class QuestionFunctional(QuestionBase):
|
10
11
|
"""A special type of question that is *not* answered by an LLM.
|
11
|
-
|
12
|
+
|
12
13
|
>>> from edsl import Scenario, Agent
|
13
|
-
|
14
|
+
|
14
15
|
# Create an instance of QuestionFunctional with the new function
|
15
16
|
>>> question = QuestionFunctional.example()
|
16
17
|
|
@@ -21,7 +22,7 @@ class QuestionFunctional(QuestionBase):
|
|
21
22
|
>>> results = question.by(scenario).by(agent).run()
|
22
23
|
>>> results.select("answer.*").to_list()[0] == 150
|
23
24
|
True
|
24
|
-
|
25
|
+
|
25
26
|
# Serialize the question to a dictionary
|
26
27
|
|
27
28
|
>>> from edsl.questions.QuestionBase import QuestionBase
|
@@ -105,8 +106,6 @@ class QuestionFunctional(QuestionBase):
|
|
105
106
|
"requires_loop": self.requires_loop,
|
106
107
|
"function_name": self.function_name,
|
107
108
|
}
|
108
|
-
|
109
|
-
|
110
109
|
|
111
110
|
@classmethod
|
112
111
|
def example(cls):
|
@@ -141,7 +140,9 @@ def main():
|
|
141
140
|
results = question.by(scenario).by(agent).run()
|
142
141
|
assert results.select("answer.*").to_list()[0] == 150
|
143
142
|
|
143
|
+
|
144
144
|
if __name__ == "__main__":
|
145
|
-
#main()
|
145
|
+
# main()
|
146
146
|
import doctest
|
147
|
-
|
147
|
+
|
148
|
+
doctest.testmod(optionflags=doctest.ELLIPSIS)
|
@@ -11,7 +11,7 @@ from edsl.questions.descriptors import QuestionOptionsDescriptor
|
|
11
11
|
|
12
12
|
class QuestionMultipleChoice(QuestionBase):
|
13
13
|
"""This question prompts the agent to select one option from a list of options.
|
14
|
-
|
14
|
+
|
15
15
|
https://docs.expectedparrot.com/en/latest/questions.html#questionmultiplechoice-class
|
16
16
|
|
17
17
|
"""
|
@@ -51,7 +51,7 @@ class QuestionMultipleChoice(QuestionBase):
|
|
51
51
|
self, answer: dict[str, Union[str, int]]
|
52
52
|
) -> dict[str, Union[str, int]]:
|
53
53
|
"""Validate the answer.
|
54
|
-
|
54
|
+
|
55
55
|
>>> q = QuestionMultipleChoice.example()
|
56
56
|
>>> q._validate_answer({"answer": 0, "comment": "I like custard"})
|
57
57
|
{'answer': 0, 'comment': 'I like custard'}
|
@@ -67,19 +67,17 @@ class QuestionMultipleChoice(QuestionBase):
|
|
67
67
|
return answer
|
68
68
|
|
69
69
|
def _translate_answer_code_to_answer(
|
70
|
-
self,
|
71
|
-
answer_code: int,
|
72
|
-
scenario: Optional["Scenario"] = None
|
70
|
+
self, answer_code: int, scenario: Optional["Scenario"] = None
|
73
71
|
):
|
74
72
|
"""Translate the answer code to the actual answer.
|
75
73
|
|
76
|
-
It is used to translate the answer code to the actual answer.
|
74
|
+
It is used to translate the answer code to the actual answer.
|
77
75
|
The question options might be templates, so they need to be rendered with the scenario.
|
78
|
-
|
76
|
+
|
79
77
|
>>> q = QuestionMultipleChoice.example()
|
80
78
|
>>> q._translate_answer_code_to_answer(0, {})
|
81
79
|
'Good'
|
82
|
-
|
80
|
+
|
83
81
|
>>> q = QuestionMultipleChoice(question_name="how_feeling", question_text="How are you?", question_options=["{{emotion[0]}}", "emotion[1]"])
|
84
82
|
>>> q._translate_answer_code_to_answer(0, {"emotion": ["Happy", "Sad"]})
|
85
83
|
'Happy'
|
@@ -92,16 +90,20 @@ class QuestionMultipleChoice(QuestionBase):
|
|
92
90
|
if isinstance(self.question_options, str):
|
93
91
|
# If dynamic options are provided like {{ options }}, render them with the scenario
|
94
92
|
from jinja2 import Environment, meta
|
93
|
+
|
95
94
|
env = Environment()
|
96
95
|
parsed_content = env.parse(self.question_options)
|
97
|
-
question_option_key = list(meta.find_undeclared_variables(parsed_content))[
|
96
|
+
question_option_key = list(meta.find_undeclared_variables(parsed_content))[
|
97
|
+
0
|
98
|
+
]
|
98
99
|
translated_options = scenario.get(question_option_key)
|
99
100
|
else:
|
100
101
|
translated_options = [
|
101
|
-
Template(str(option)).render(scenario)
|
102
|
+
Template(str(option)).render(scenario)
|
103
|
+
for option in self.question_options
|
102
104
|
]
|
103
|
-
#print("Translated options:", translated_options)
|
104
|
-
#breakpoint()
|
105
|
+
# print("Translated options:", translated_options)
|
106
|
+
# breakpoint()
|
105
107
|
return translated_options[int(answer_code)]
|
106
108
|
|
107
109
|
def _simulate_answer(
|
edsl/questions/descriptors.py
CHANGED
@@ -249,6 +249,7 @@ class QuestionOptionsDescriptor(BaseDescriptor):
|
|
249
249
|
|
250
250
|
def __init__(self, question_options: List[str]):
|
251
251
|
self.question_options = question_options
|
252
|
+
|
252
253
|
return TestQuestion
|
253
254
|
|
254
255
|
def __init__(
|
@@ -264,16 +265,16 @@ class QuestionOptionsDescriptor(BaseDescriptor):
|
|
264
265
|
|
265
266
|
def validate(self, value: Any, instance) -> None:
|
266
267
|
"""Validate the question options.
|
267
|
-
|
268
|
+
|
268
269
|
>>> q_class = QuestionOptionsDescriptor.example()
|
269
270
|
>>> _ = q_class(["a", "b", "c"])
|
270
271
|
>>> _ = q_class(["a", "b", "c", "d", "d"])
|
271
272
|
Traceback (most recent call last):
|
272
273
|
...
|
273
274
|
edsl.exceptions.questions.QuestionCreationValidationError: Question options must be unique (got ['a', 'b', 'c', 'd', 'd']).
|
274
|
-
|
275
|
+
|
275
276
|
We allow dynamic question options, which are strings of the form '{{ question_options }}'.
|
276
|
-
|
277
|
+
|
277
278
|
>>> _ = q_class("{{dynamic_options}}")
|
278
279
|
>>> _ = q_class("dynamic_options")
|
279
280
|
Traceback (most recent call last):
|
@@ -373,7 +374,8 @@ class QuestionTextDescriptor(BaseDescriptor):
|
|
373
374
|
UserWarning,
|
374
375
|
)
|
375
376
|
|
377
|
+
|
376
378
|
if __name__ == "__main__":
|
377
379
|
import doctest
|
378
380
|
|
379
|
-
doctest.testmod(optionflags=doctest.ELLIPSIS)
|
381
|
+
doctest.testmod(optionflags=doctest.ELLIPSIS)
|