edsl 0.1.27.dev2__py3-none-any.whl → 0.1.28__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/Base.py +99 -22
- edsl/BaseDiff.py +260 -0
- edsl/__init__.py +4 -0
- edsl/__version__.py +1 -1
- edsl/agents/Agent.py +26 -5
- edsl/agents/AgentList.py +62 -7
- edsl/agents/Invigilator.py +4 -9
- edsl/agents/InvigilatorBase.py +5 -5
- edsl/agents/descriptors.py +3 -1
- edsl/conjure/AgentConstructionMixin.py +152 -0
- edsl/conjure/Conjure.py +56 -0
- edsl/conjure/InputData.py +628 -0
- edsl/conjure/InputDataCSV.py +48 -0
- edsl/conjure/InputDataMixinQuestionStats.py +182 -0
- edsl/conjure/InputDataPyRead.py +91 -0
- edsl/conjure/InputDataSPSS.py +8 -0
- edsl/conjure/InputDataStata.py +8 -0
- edsl/conjure/QuestionOptionMixin.py +76 -0
- edsl/conjure/QuestionTypeMixin.py +23 -0
- edsl/conjure/RawQuestion.py +65 -0
- edsl/conjure/SurveyResponses.py +7 -0
- edsl/conjure/__init__.py +9 -4
- edsl/conjure/examples/placeholder.txt +0 -0
- edsl/conjure/naming_utilities.py +263 -0
- edsl/conjure/utilities.py +165 -28
- edsl/conversation/Conversation.py +238 -0
- edsl/conversation/car_buying.py +58 -0
- edsl/conversation/mug_negotiation.py +81 -0
- edsl/conversation/next_speaker_utilities.py +93 -0
- edsl/coop/coop.py +191 -12
- edsl/coop/utils.py +20 -2
- edsl/data/Cache.py +55 -17
- edsl/data/CacheHandler.py +10 -9
- edsl/inference_services/AnthropicService.py +1 -0
- edsl/inference_services/DeepInfraService.py +20 -13
- edsl/inference_services/GoogleService.py +7 -1
- edsl/inference_services/InferenceServicesCollection.py +33 -7
- edsl/inference_services/OpenAIService.py +17 -10
- edsl/inference_services/models_available_cache.py +69 -0
- edsl/inference_services/rate_limits_cache.py +25 -0
- edsl/inference_services/write_available.py +10 -0
- edsl/jobs/Jobs.py +240 -36
- edsl/jobs/buckets/BucketCollection.py +9 -3
- edsl/jobs/interviews/Interview.py +4 -1
- edsl/jobs/interviews/InterviewTaskBuildingMixin.py +24 -10
- edsl/jobs/interviews/retry_management.py +4 -4
- edsl/jobs/runners/JobsRunnerAsyncio.py +87 -45
- edsl/jobs/runners/JobsRunnerStatusData.py +3 -3
- edsl/jobs/tasks/QuestionTaskCreator.py +4 -2
- edsl/language_models/LanguageModel.py +37 -44
- edsl/language_models/ModelList.py +96 -0
- edsl/language_models/registry.py +14 -0
- edsl/language_models/repair.py +95 -24
- edsl/notebooks/Notebook.py +119 -31
- edsl/questions/QuestionBase.py +109 -12
- edsl/questions/descriptors.py +5 -2
- edsl/questions/question_registry.py +7 -0
- edsl/results/Result.py +20 -8
- edsl/results/Results.py +85 -11
- edsl/results/ResultsDBMixin.py +3 -6
- edsl/results/ResultsExportMixin.py +47 -16
- edsl/results/ResultsToolsMixin.py +5 -5
- edsl/scenarios/Scenario.py +59 -5
- edsl/scenarios/ScenarioList.py +97 -40
- edsl/study/ObjectEntry.py +97 -0
- edsl/study/ProofOfWork.py +110 -0
- edsl/study/SnapShot.py +77 -0
- edsl/study/Study.py +491 -0
- edsl/study/__init__.py +2 -0
- edsl/surveys/Survey.py +79 -31
- edsl/surveys/SurveyExportMixin.py +21 -3
- edsl/utilities/__init__.py +1 -0
- edsl/utilities/gcp_bucket/__init__.py +0 -0
- edsl/utilities/gcp_bucket/cloud_storage.py +96 -0
- edsl/utilities/gcp_bucket/simple_example.py +9 -0
- edsl/utilities/interface.py +24 -28
- edsl/utilities/repair_functions.py +28 -0
- edsl/utilities/utilities.py +57 -2
- {edsl-0.1.27.dev2.dist-info → edsl-0.1.28.dist-info}/METADATA +43 -17
- {edsl-0.1.27.dev2.dist-info → edsl-0.1.28.dist-info}/RECORD +83 -55
- edsl-0.1.28.dist-info/entry_points.txt +3 -0
- edsl/conjure/RawResponseColumn.py +0 -327
- edsl/conjure/SurveyBuilder.py +0 -308
- edsl/conjure/SurveyBuilderCSV.py +0 -78
- edsl/conjure/SurveyBuilderSPSS.py +0 -118
- edsl/data/RemoteDict.py +0 -103
- {edsl-0.1.27.dev2.dist-info → edsl-0.1.28.dist-info}/LICENSE +0 -0
- {edsl-0.1.27.dev2.dist-info → edsl-0.1.28.dist-info}/WHEEL +0 -0
edsl/language_models/repair.py
CHANGED
@@ -1,14 +1,19 @@
|
|
1
1
|
import json
|
2
2
|
import asyncio
|
3
|
+
import warnings
|
4
|
+
from rich import print
|
5
|
+
from rich.console import Console
|
6
|
+
from rich.syntax import Syntax
|
3
7
|
|
4
8
|
from edsl.utilities.utilities import clean_json
|
5
9
|
|
10
|
+
from edsl.utilities.repair_functions import extract_json_from_string
|
6
11
|
|
7
|
-
async def async_repair(bad_json, error_message=""):
|
8
|
-
s = clean_json(bad_json)
|
9
|
-
from edsl import Model
|
10
12
|
|
11
|
-
|
13
|
+
async def async_repair(
|
14
|
+
bad_json, error_message="", user_prompt=None, system_prompt=None, cache=None
|
15
|
+
):
|
16
|
+
s = clean_json(bad_json)
|
12
17
|
|
13
18
|
try:
|
14
19
|
# this is the OpenAI version, but that's fine
|
@@ -17,56 +22,122 @@ async def async_repair(bad_json, error_message=""):
|
|
17
22
|
except json.JSONDecodeError:
|
18
23
|
valid_dict = {}
|
19
24
|
success = False
|
20
|
-
# print("Replacing control characters didn't work. Trying
|
25
|
+
# print("Replacing control characters didn't work. Trying extracting the sub-string.")
|
21
26
|
else:
|
22
27
|
return valid_dict, success
|
23
28
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
29
|
+
try:
|
30
|
+
valid_dict = extract_json_from_string(s)
|
31
|
+
success = True
|
32
|
+
except ValueError:
|
33
|
+
valid_dict = {}
|
34
|
+
success = False
|
35
|
+
else:
|
36
|
+
return valid_dict, success
|
28
37
|
|
29
|
-
|
30
|
-
prompt += f" Parsing error message: {error_message}"
|
38
|
+
from edsl import Model
|
31
39
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
40
|
+
m = Model()
|
41
|
+
|
42
|
+
from edsl import QuestionExtract
|
43
|
+
|
44
|
+
with warnings.catch_warnings():
|
45
|
+
warnings.simplefilter("ignore", UserWarning)
|
46
|
+
|
47
|
+
q = QuestionExtract(
|
48
|
+
question_text="""
|
49
|
+
A language model was supposed to respond to a question.
|
50
|
+
The response should have been JSON object with an answer to a question and some commentary.
|
51
|
+
|
52
|
+
It should have retured a string like this:
|
53
|
+
|
54
|
+
'{'answer': 'The answer to the question.', 'comment': 'Some commentary.'}'
|
55
|
+
|
56
|
+
or:
|
57
|
+
|
58
|
+
'{'answer': 'The answer to the question.'}'
|
59
|
+
|
60
|
+
The answer field is very like an integer number. The comment field is always string.
|
61
|
+
|
62
|
+
You job is to return just the repaired JSON object that the model should have returned, properly formatted.
|
63
|
+
|
64
|
+
- It might have included some preliminary comments.
|
65
|
+
- It might have included some control characters.
|
66
|
+
- It might have included some extraneous text.
|
67
|
+
|
68
|
+
DO NOT include any extraneous text in your response. Just return the repaired JSON object.
|
69
|
+
Do not preface the JSON object with any text. Just return the JSON object.
|
70
|
+
|
71
|
+
Bad answer: """
|
72
|
+
+ str(bad_json)
|
73
|
+
+ "The model received a user prompt of: '"
|
74
|
+
+ str(user_prompt)
|
75
|
+
+ """'
|
76
|
+
The model received a system prompt of: ' """
|
77
|
+
+ str(system_prompt)
|
78
|
+
+ """
|
79
|
+
'
|
80
|
+
Please return the repaired JSON object, following the instructions the original model should have followed, though
|
81
|
+
using 'new_answer' a nd 'new_comment' as the keys.""",
|
82
|
+
answer_template={
|
83
|
+
"new_answer": "<number, string, list, etc.>",
|
84
|
+
"new_comment": "Model's comments",
|
85
|
+
},
|
86
|
+
question_name="model_repair",
|
36
87
|
)
|
37
|
-
|
38
|
-
|
88
|
+
|
89
|
+
results = await q.run_async(cache=cache)
|
39
90
|
|
40
91
|
try:
|
41
92
|
# this is the OpenAI version, but that's fine
|
42
|
-
valid_dict = json.loads(results
|
93
|
+
valid_dict = json.loads(json.dumps(results))
|
43
94
|
success = True
|
95
|
+
# this is to deal with the fact that the model returns the answer and comment as new_answer and new_comment
|
96
|
+
valid_dict["answer"] = valid_dict.pop("new_answer")
|
97
|
+
valid_dict["comment"] = valid_dict.pop("new_comment")
|
44
98
|
except json.JSONDecodeError:
|
45
99
|
valid_dict = {}
|
46
100
|
success = False
|
101
|
+
console = Console()
|
102
|
+
error_message = (
|
103
|
+
f"All repairs. failed. LLM Model given [red]{str(bad_json)}[/red]"
|
104
|
+
)
|
105
|
+
console.print(" " + error_message)
|
106
|
+
model_returned = results["choices"][0]["message"]["content"]
|
107
|
+
console.print(f"LLM Model returned: [blue]{model_returned}[/blue]")
|
47
108
|
|
48
109
|
return valid_dict, success
|
49
110
|
|
50
111
|
|
51
|
-
def repair_wrapper(
|
112
|
+
def repair_wrapper(
|
113
|
+
bad_json, error_message="", user_prompt=None, system_prompt=None, cache=None
|
114
|
+
):
|
52
115
|
try:
|
53
116
|
loop = asyncio.get_event_loop()
|
54
117
|
if loop.is_running():
|
55
118
|
# Add repair as a task to the running loop
|
56
|
-
task = loop.create_task(
|
119
|
+
task = loop.create_task(
|
120
|
+
async_repair(bad_json, error_message, user_prompt, system_prompt, cache)
|
121
|
+
)
|
57
122
|
return task
|
58
123
|
else:
|
59
124
|
# Run a new event loop for repair
|
60
|
-
return loop.run_until_complete(
|
125
|
+
return loop.run_until_complete(
|
126
|
+
async_repair(bad_json, error_message, user_prompt, system_prompt, cache)
|
127
|
+
)
|
61
128
|
except RuntimeError:
|
62
129
|
# Create a new event loop if one is not already available
|
63
130
|
loop = asyncio.new_event_loop()
|
64
131
|
asyncio.set_event_loop(loop)
|
65
|
-
return loop.run_until_complete(
|
132
|
+
return loop.run_until_complete(
|
133
|
+
async_repair(bad_json, error_message, user_prompt, system_prompt, cache)
|
134
|
+
)
|
66
135
|
|
67
136
|
|
68
|
-
def repair(
|
69
|
-
|
137
|
+
def repair(
|
138
|
+
bad_json, error_message="", user_prompt=None, system_prompt=None, cache=None
|
139
|
+
):
|
140
|
+
return repair_wrapper(bad_json, error_message, user_prompt, system_prompt, cache)
|
70
141
|
|
71
142
|
|
72
143
|
# Example usage:
|
edsl/notebooks/Notebook.py
CHANGED
@@ -1,5 +1,8 @@
|
|
1
|
-
"""A Notebook is
|
1
|
+
"""A Notebook is a utility class that allows you to easily share/pull ipynbs from Coop."""
|
2
2
|
|
3
|
+
import json
|
4
|
+
import nbformat
|
5
|
+
from nbconvert import HTMLExporter
|
3
6
|
from typing import Dict, List, Optional
|
4
7
|
from rich.table import Table
|
5
8
|
from edsl.Base import Base
|
@@ -14,43 +17,58 @@ class Notebook(Base):
|
|
14
17
|
A Notebook is a utility class that allows you to easily share/pull ipynbs from Coop.
|
15
18
|
"""
|
16
19
|
|
17
|
-
|
20
|
+
default_name = "notebook"
|
21
|
+
|
22
|
+
def __init__(
|
23
|
+
self,
|
24
|
+
data: Optional[Dict] = None,
|
25
|
+
path: Optional[str] = None,
|
26
|
+
name: Optional[str] = None,
|
27
|
+
):
|
18
28
|
"""
|
19
29
|
Initialize a new Notebook.
|
20
|
-
|
21
|
-
|
30
|
+
|
31
|
+
:param data: A dictionary representing the notebook data.
|
32
|
+
This dictionary must conform to the official Jupyter Notebook format, as defined by nbformat.
|
33
|
+
:param path: A filepath from which to load the notebook.
|
34
|
+
If no path is provided, assume this code is run in a notebook and try to load the current notebook from file.
|
35
|
+
:param name: A name for the Notebook.
|
22
36
|
"""
|
37
|
+
# Load current notebook path as fallback (VS Code only)
|
38
|
+
path = path or globals().get("__vsc_ipynb_file__")
|
23
39
|
if data is not None:
|
40
|
+
nbformat.validate(data)
|
24
41
|
self.data = data
|
25
42
|
elif path is not None:
|
26
|
-
|
27
|
-
|
28
|
-
self.data =
|
43
|
+
with open(path, mode="r", encoding="utf-8") as f:
|
44
|
+
data = nbformat.read(f, as_version=4)
|
45
|
+
self.data = json.loads(json.dumps(data))
|
29
46
|
else:
|
30
|
-
#
|
31
|
-
|
32
|
-
|
33
|
-
|
47
|
+
# TODO: Support for IDEs other than VSCode
|
48
|
+
raise NotImplementedError(
|
49
|
+
"Cannot create a notebook from within itself in this development environment"
|
50
|
+
)
|
34
51
|
|
35
|
-
#
|
52
|
+
# TODO: perhaps add sanity check function
|
36
53
|
# 1. could check if the notebook is a valid notebook
|
37
54
|
# 2. could check notebook uses EDSL
|
38
55
|
# ....
|
39
56
|
|
57
|
+
self.name = name or self.default_name
|
58
|
+
|
40
59
|
def __eq__(self, other):
|
41
60
|
"""
|
42
61
|
Check if two Notebooks are equal.
|
62
|
+
This only checks the notebook data.
|
43
63
|
"""
|
44
64
|
return self.data == other.data
|
45
65
|
|
46
66
|
@add_edsl_version
|
47
67
|
def to_dict(self) -> dict:
|
48
68
|
"""
|
49
|
-
Convert to a dictionary.
|
50
|
-
AF: here you will create a dict from which self.from_dict can recreate the object.
|
51
|
-
AF: the decorator will add the edsl_version to the dict.
|
69
|
+
Convert a Notebook to a dictionary.
|
52
70
|
"""
|
53
|
-
return {"data": self.data}
|
71
|
+
return {"name": self.name, "data": self.data}
|
54
72
|
|
55
73
|
@classmethod
|
56
74
|
@remove_edsl_version
|
@@ -58,12 +76,17 @@ class Notebook(Base):
|
|
58
76
|
"""
|
59
77
|
Convert a dictionary representation of a Notebook to a Notebook object.
|
60
78
|
"""
|
61
|
-
return cls(data=d["data"])
|
79
|
+
return cls(data=d["data"], name=d["name"])
|
80
|
+
|
81
|
+
def to_file(self, path: str):
|
82
|
+
"""
|
83
|
+
Save the notebook at the specified filepath.
|
84
|
+
"""
|
85
|
+
nbformat.write(nbformat.from_dict(self.data), fp=path)
|
62
86
|
|
63
87
|
def print(self):
|
64
88
|
"""
|
65
89
|
Print the notebook.
|
66
|
-
AF: not sure how this should behave for a notebook
|
67
90
|
"""
|
68
91
|
from rich import print_json
|
69
92
|
import json
|
@@ -72,45 +95,110 @@ class Notebook(Base):
|
|
72
95
|
|
73
96
|
def __repr__(self):
|
74
97
|
"""
|
75
|
-
|
98
|
+
Return representation of Notebook.
|
76
99
|
"""
|
77
|
-
return f
|
100
|
+
return f'Notebook(data={self.data}, name="""{self.name}""")'
|
78
101
|
|
79
102
|
def _repr_html_(self):
|
80
103
|
"""
|
81
|
-
|
104
|
+
Return HTML representation of Notebook.
|
82
105
|
"""
|
83
|
-
|
106
|
+
notebook = nbformat.from_dict(self.data)
|
107
|
+
html_exporter = HTMLExporter(template_name="basic")
|
108
|
+
(body, _) = html_exporter.from_notebook_node(notebook)
|
109
|
+
return body
|
84
110
|
|
85
|
-
|
111
|
+
def _table(self) -> tuple[dict, list]:
|
112
|
+
"""
|
113
|
+
Prepare generic table data.
|
114
|
+
"""
|
115
|
+
table_data = []
|
116
|
+
|
117
|
+
notebook_preview = ""
|
118
|
+
for cell in self.data["cells"]:
|
119
|
+
if "source" in cell:
|
120
|
+
notebook_preview += f"{cell['source']}\n"
|
121
|
+
if len(notebook_preview) > 1000:
|
122
|
+
notebook_preview = f"{notebook_preview[:1000]} [...]"
|
123
|
+
break
|
124
|
+
notebook_preview = notebook_preview.rstrip()
|
125
|
+
|
126
|
+
table_data.append(
|
127
|
+
{
|
128
|
+
"Attribute": "name",
|
129
|
+
"Value": repr(self.name),
|
130
|
+
}
|
131
|
+
)
|
132
|
+
table_data.append(
|
133
|
+
{
|
134
|
+
"Attribute": "notebook_preview",
|
135
|
+
"Value": notebook_preview,
|
136
|
+
}
|
137
|
+
)
|
138
|
+
|
139
|
+
column_names = ["Attribute", "Value"]
|
140
|
+
return table_data, column_names
|
86
141
|
|
87
142
|
def rich_print(self) -> "Table":
|
88
143
|
"""
|
89
|
-
|
144
|
+
Display a Notebook as a rich table.
|
90
145
|
"""
|
91
|
-
|
146
|
+
table_data, column_names = self._table()
|
147
|
+
table = Table(title=f"{self.__class__.__name__} Attributes")
|
148
|
+
for column in column_names:
|
149
|
+
table.add_column(column, style="bold")
|
150
|
+
|
151
|
+
for row in table_data:
|
152
|
+
row_data = [row[column] for column in column_names]
|
153
|
+
table.add_row(*row_data)
|
154
|
+
|
155
|
+
return table
|
92
156
|
|
93
157
|
@classmethod
|
94
158
|
def example(cls) -> "Notebook":
|
95
159
|
"""
|
96
160
|
Return an example Notebook.
|
97
|
-
AF: add a simple custom example here
|
98
161
|
"""
|
99
|
-
|
162
|
+
cells = [
|
163
|
+
{
|
164
|
+
"cell_type": "markdown",
|
165
|
+
"metadata": dict(),
|
166
|
+
"source": "# Test notebook",
|
167
|
+
},
|
168
|
+
{
|
169
|
+
"cell_type": "code",
|
170
|
+
"execution_count": 1,
|
171
|
+
"metadata": dict(),
|
172
|
+
"outputs": [
|
173
|
+
{
|
174
|
+
"name": "stdout",
|
175
|
+
"output_type": "stream",
|
176
|
+
"text": "Hello world!\n",
|
177
|
+
}
|
178
|
+
],
|
179
|
+
"source": 'print("Hello world!")',
|
180
|
+
},
|
181
|
+
]
|
182
|
+
data = {
|
183
|
+
"metadata": dict(),
|
184
|
+
"nbformat": 4,
|
185
|
+
"nbformat_minor": 4,
|
186
|
+
"cells": cells,
|
187
|
+
}
|
188
|
+
return cls(data=data)
|
100
189
|
|
101
190
|
def code(self) -> List[str]:
|
102
191
|
"""
|
103
192
|
Return the code that could be used to create this Notebook.
|
104
|
-
AF: Again, not sure
|
105
193
|
"""
|
106
194
|
lines = []
|
107
|
-
lines.append("from edsl
|
108
|
-
lines.append(f
|
195
|
+
lines.append("from edsl import Notebook")
|
196
|
+
lines.append(f'nb = Notebook(data={self.data}, name="""{self.name}""")')
|
109
197
|
return lines
|
110
198
|
|
111
199
|
|
112
200
|
if __name__ == "__main__":
|
113
|
-
from edsl
|
201
|
+
from edsl import Notebook
|
114
202
|
|
115
203
|
notebook = Notebook.example()
|
116
204
|
assert notebook == notebook.from_dict(notebook.to_dict())
|
edsl/questions/QuestionBase.py
CHANGED
@@ -3,7 +3,8 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
from abc import ABC, abstractmethod
|
5
5
|
from rich.table import Table
|
6
|
-
from typing import Any, Type, Optional
|
6
|
+
from typing import Any, Type, Optional, List, Callable
|
7
|
+
import copy
|
7
8
|
|
8
9
|
from edsl.exceptions import (
|
9
10
|
QuestionResponseValidationError,
|
@@ -15,6 +16,7 @@ from edsl.prompts.registry import get_classes as prompt_lookup
|
|
15
16
|
from edsl.questions.AnswerValidatorMixin import AnswerValidatorMixin
|
16
17
|
from edsl.questions.RegisterQuestionsMeta import RegisterQuestionsMeta
|
17
18
|
from edsl.Base import PersistenceMixin, RichPrintingMixin
|
19
|
+
from edsl.BaseDiff import BaseDiff, BaseDiffCollection
|
18
20
|
|
19
21
|
from edsl.questions.SimpleAskMixin import SimpleAskMixin
|
20
22
|
from edsl.utilities.decorators import add_edsl_version, remove_edsl_version
|
@@ -37,6 +39,12 @@ class QuestionBase(
|
|
37
39
|
"""Get an attribute of the question."""
|
38
40
|
return getattr(self, key)
|
39
41
|
|
42
|
+
def __hash__(self) -> int:
|
43
|
+
"""Return a hash of the question."""
|
44
|
+
from edsl.utilities.utilities import dict_hash
|
45
|
+
|
46
|
+
return dict_hash(self._to_dict())
|
47
|
+
|
40
48
|
def _repr_html_(self):
|
41
49
|
from edsl.utilities.utilities import data_to_html
|
42
50
|
|
@@ -49,6 +57,35 @@ class QuestionBase(
|
|
49
57
|
|
50
58
|
return data_to_html(data)
|
51
59
|
|
60
|
+
def apply_function(self, func: Callable, exclude_components=None) -> QuestionBase:
|
61
|
+
"""Apply a function to the question parts
|
62
|
+
|
63
|
+
>>> from edsl.questions import QuestionFreeText
|
64
|
+
>>> q = QuestionFreeText(question_name = "color", question_text = "What is your favorite color?")
|
65
|
+
>>> shouting = lambda x: x.upper()
|
66
|
+
>>> q.apply_function(shouting)
|
67
|
+
Question('free_text', question_name = \"""color\""", question_text = \"""WHAT IS YOUR FAVORITE COLOR?\""")
|
68
|
+
|
69
|
+
"""
|
70
|
+
if exclude_components is None:
|
71
|
+
exclude_components = ["question_name", "question_type"]
|
72
|
+
|
73
|
+
d = copy.deepcopy(self._to_dict())
|
74
|
+
for key, value in d.items():
|
75
|
+
if key in exclude_components:
|
76
|
+
continue
|
77
|
+
if isinstance(value, dict):
|
78
|
+
for k, v in value.items():
|
79
|
+
value[k] = func(v)
|
80
|
+
d[key] = value
|
81
|
+
continue
|
82
|
+
if isinstance(value, list):
|
83
|
+
value = [func(v) for v in value]
|
84
|
+
d[key] = value
|
85
|
+
continue
|
86
|
+
d[key] = func(value)
|
87
|
+
return QuestionBase.from_dict(d)
|
88
|
+
|
52
89
|
@property
|
53
90
|
def data(self) -> dict:
|
54
91
|
"""Return a dictionary of question attributes **except** for question_type."""
|
@@ -101,6 +138,33 @@ class QuestionBase(
|
|
101
138
|
self._model_instructions = {}
|
102
139
|
return self._model_instructions
|
103
140
|
|
141
|
+
def _all_text(self) -> str:
|
142
|
+
"""Return the question text."""
|
143
|
+
txt = ""
|
144
|
+
for key, value in self.data.items():
|
145
|
+
if isinstance(value, str):
|
146
|
+
txt += value
|
147
|
+
elif isinstance(value, list):
|
148
|
+
txt += "".join(str(value))
|
149
|
+
return txt
|
150
|
+
|
151
|
+
@property
|
152
|
+
def parameters(self) -> set[str]:
|
153
|
+
"""Return the parameters of the question."""
|
154
|
+
from jinja2 import Environment, meta
|
155
|
+
|
156
|
+
env = Environment()
|
157
|
+
# Parse the template
|
158
|
+
txt = self._all_text()
|
159
|
+
# txt = self.question_text
|
160
|
+
# if hasattr(self, "question_options"):
|
161
|
+
# txt += " ".join(self.question_options)
|
162
|
+
parsed_content = env.parse(txt)
|
163
|
+
# Extract undeclared variables
|
164
|
+
variables = meta.find_undeclared_variables(parsed_content)
|
165
|
+
# Return as a list
|
166
|
+
return set(variables)
|
167
|
+
|
104
168
|
@model_instructions.setter
|
105
169
|
def model_instructions(self, data: dict):
|
106
170
|
"""Set the model-specific instructions for the question."""
|
@@ -167,13 +231,17 @@ class QuestionBase(
|
|
167
231
|
############################
|
168
232
|
# Serialization methods
|
169
233
|
############################
|
170
|
-
|
171
|
-
def to_dict(self) -> dict[str, Any]:
|
234
|
+
def _to_dict(self):
|
172
235
|
"""Convert the question to a dictionary that includes the question type (used in deserialization)."""
|
173
236
|
candidate_data = self.data.copy()
|
174
237
|
candidate_data["question_type"] = self.question_type
|
175
238
|
return candidate_data
|
176
239
|
|
240
|
+
@add_edsl_version
|
241
|
+
def to_dict(self) -> dict[str, Any]:
|
242
|
+
"""Convert the question to a dictionary that includes the question type (used in deserialization)."""
|
243
|
+
return self._to_dict()
|
244
|
+
|
177
245
|
@classmethod
|
178
246
|
@remove_edsl_version
|
179
247
|
def from_dict(cls, data: dict) -> Type[QuestionBase]:
|
@@ -211,6 +279,10 @@ class QuestionBase(
|
|
211
279
|
|
212
280
|
return question_class(**local_data)
|
213
281
|
|
282
|
+
def copy(self) -> Type[QuestionBase]:
|
283
|
+
"""Return a deep copy of the question."""
|
284
|
+
return copy.deepcopy(self)
|
285
|
+
|
214
286
|
############################
|
215
287
|
# Dunder methods
|
216
288
|
############################
|
@@ -220,6 +292,25 @@ class QuestionBase(
|
|
220
292
|
|
221
293
|
print_json(json.dumps(self.to_dict()))
|
222
294
|
|
295
|
+
def __call__(self, just_answer=True, model=None, agent=None, **kwargs):
|
296
|
+
"""Call the question."""
|
297
|
+
survey = self.to_survey()
|
298
|
+
results = survey(model=model, agent=agent, **kwargs)
|
299
|
+
if just_answer:
|
300
|
+
return results.select(f"answer.{self.question_name}").first()
|
301
|
+
else:
|
302
|
+
return results
|
303
|
+
|
304
|
+
async def run_async(self, just_answer=True, model=None, agent=None, **kwargs):
|
305
|
+
"""Call the question."""
|
306
|
+
survey = self.to_survey()
|
307
|
+
## asyncio.run(survey.async_call());
|
308
|
+
results = await survey.run_async(model=model, agent=agent, **kwargs)
|
309
|
+
if just_answer:
|
310
|
+
return results.select(f"answer.{self.question_name}").first()
|
311
|
+
else:
|
312
|
+
return results
|
313
|
+
|
223
314
|
def __repr__(self) -> str:
|
224
315
|
"""Return a string representation of the question. Should be able to be used to reconstruct the question."""
|
225
316
|
class_name = self.__class__.__name__
|
@@ -237,21 +328,27 @@ class QuestionBase(
|
|
237
328
|
return False
|
238
329
|
return self.to_dict() == other.to_dict()
|
239
330
|
|
331
|
+
def __sub__(self, other) -> BaseDiff:
|
332
|
+
"""Return the difference between two objects."""
|
333
|
+
|
334
|
+
return BaseDiff(other, self)
|
335
|
+
|
240
336
|
# TODO: Throws an error that should be addressed at QuestionFunctional
|
241
|
-
def __add__(self,
|
337
|
+
def __add__(self, other_question_or_diff):
|
242
338
|
"""
|
243
339
|
Compose two questions into a single question.
|
244
340
|
|
245
|
-
|
246
|
-
|
247
|
-
>>> from edsl.questions.QuestionNumerical import QuestionNumerical
|
248
|
-
>>> q1 = QuestionFreeText(question_text = "What is the capital of {{country}}", question_name = "capital")
|
249
|
-
>>> q2 = QuestionNumerical(question_text = "What is the population of {{capital}}, in millions. Please round", question_name = "population")
|
250
|
-
>>> q3 = q1 + q2
|
341
|
+
TODO: Probably getting deprecated.
|
342
|
+
|
251
343
|
"""
|
344
|
+
if isinstance(other_question_or_diff, BaseDiff) or isinstance(
|
345
|
+
other_question_or_diff, BaseDiffCollection
|
346
|
+
):
|
347
|
+
return other_question_or_diff.apply(self)
|
348
|
+
|
252
349
|
from edsl.questions import compose_questions
|
253
350
|
|
254
|
-
return compose_questions(self,
|
351
|
+
return compose_questions(self, other_question_or_diff)
|
255
352
|
|
256
353
|
@abstractmethod
|
257
354
|
def _validate_answer(self, answer: dict[str, str]):
|
@@ -279,7 +376,7 @@ class QuestionBase(
|
|
279
376
|
############################
|
280
377
|
# Forward methods
|
281
378
|
############################
|
282
|
-
def add_question(self, other:
|
379
|
+
def add_question(self, other: QuestionBase) -> "Survey":
|
283
380
|
"""Add a question to this question by turning them into a survey with two questions."""
|
284
381
|
from edsl.surveys.Survey import Survey
|
285
382
|
|
edsl/questions/descriptors.py
CHANGED
@@ -331,6 +331,9 @@ class QuestionTextDescriptor(BaseDescriptor):
|
|
331
331
|
if not isinstance(value, str):
|
332
332
|
raise Exception("Question must be a string!")
|
333
333
|
if contains_single_braced_substring(value):
|
334
|
-
|
335
|
-
|
334
|
+
import warnings
|
335
|
+
|
336
|
+
warnings.warn(
|
337
|
+
f"WARNING: Question text contains a single-braced substring: If you intended to parameterize the question with a Scenario this should be changed to a double-braced substring, e.g. {{variable}}.\nSee details on constructing Scenarios in the docs: https://docs.expectedparrot.com/en/latest/scenarios.html",
|
338
|
+
UserWarning,
|
336
339
|
)
|
@@ -52,6 +52,13 @@ class Question(metaclass=Meta):
|
|
52
52
|
instance.__init__(*args, **kwargs)
|
53
53
|
return instance
|
54
54
|
|
55
|
+
@classmethod
|
56
|
+
def example(cls, question_type: str):
|
57
|
+
"""Return an example question of the given type."""
|
58
|
+
get_question_classes = RegisterQuestionsMeta.question_types_to_classes()
|
59
|
+
q = get_question_classes.get(question_type, None)
|
60
|
+
return q.example()
|
61
|
+
|
55
62
|
@classmethod
|
56
63
|
def pull(cls, id_or_url: str):
|
57
64
|
"""Pull the object from coop."""
|
edsl/results/Result.py
CHANGED
@@ -58,6 +58,8 @@ class Result(Base, UserDict):
|
|
58
58
|
|
59
59
|
The answer dictionary has the structure:
|
60
60
|
|
61
|
+
>>> import warnings
|
62
|
+
>>> warnings.simplefilter("ignore", UserWarning)
|
61
63
|
>>> Result.example().answer
|
62
64
|
{'how_feeling': 'OK', 'how_feeling_comment': 'This is a real survey response from a human.', 'how_feeling_yesterday': 'Great', 'how_feeling_yesterday_comment': 'This is a real survey response from a human.'}
|
63
65
|
|
@@ -237,14 +239,8 @@ class Result(Base, UserDict):
|
|
237
239
|
###############
|
238
240
|
# Serialization
|
239
241
|
###############
|
240
|
-
|
241
|
-
|
242
|
-
"""Return a dictionary representation of the Result object.
|
243
|
-
|
244
|
-
>>> r = Result.example()
|
245
|
-
>>> r.to_dict()['scenario']
|
246
|
-
{'period': 'morning', 'edsl_version': '...', 'edsl_class_name': 'Scenario'}
|
247
|
-
"""
|
242
|
+
def _to_dict(self) -> dict[str, Any]:
|
243
|
+
"""Return a dictionary representation of the Result object."""
|
248
244
|
d = {}
|
249
245
|
for key, value in self.items():
|
250
246
|
if hasattr(value, "to_dict"):
|
@@ -262,6 +258,22 @@ class Result(Base, UserDict):
|
|
262
258
|
d[key] = new_prompt_dict
|
263
259
|
return d
|
264
260
|
|
261
|
+
@add_edsl_version
|
262
|
+
def to_dict(self) -> dict[str, Any]:
|
263
|
+
"""Return a dictionary representation of the Result object.
|
264
|
+
|
265
|
+
>>> r = Result.example()
|
266
|
+
>>> r.to_dict()['scenario']
|
267
|
+
{'period': 'morning', 'edsl_version': '...', 'edsl_class_name': 'Scenario'}
|
268
|
+
"""
|
269
|
+
return self._to_dict()
|
270
|
+
|
271
|
+
def __hash__(self):
|
272
|
+
"""Return a hash of the Result object."""
|
273
|
+
from edsl.utilities.utilities import dict_hash
|
274
|
+
|
275
|
+
return dict_hash(self._to_dict())
|
276
|
+
|
265
277
|
@classmethod
|
266
278
|
@remove_edsl_version
|
267
279
|
def from_dict(self, json_dict: dict) -> Result:
|