edsl 0.1.33.dev2__py3-none-any.whl → 0.1.34__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 +24 -14
- edsl/__init__.py +1 -0
- edsl/__version__.py +1 -1
- edsl/agents/Agent.py +6 -6
- edsl/agents/Invigilator.py +28 -6
- edsl/agents/InvigilatorBase.py +8 -27
- edsl/agents/{PromptConstructionMixin.py → PromptConstructor.py} +150 -182
- edsl/agents/prompt_helpers.py +129 -0
- edsl/config.py +26 -34
- edsl/coop/coop.py +14 -4
- edsl/data_transfer_models.py +26 -73
- edsl/enums.py +2 -0
- edsl/inference_services/AnthropicService.py +5 -2
- edsl/inference_services/AwsBedrock.py +5 -2
- edsl/inference_services/AzureAI.py +5 -2
- edsl/inference_services/GoogleService.py +108 -33
- edsl/inference_services/InferenceServiceABC.py +44 -13
- edsl/inference_services/MistralAIService.py +5 -2
- edsl/inference_services/OpenAIService.py +10 -6
- edsl/inference_services/TestService.py +34 -16
- edsl/inference_services/TogetherAIService.py +170 -0
- edsl/inference_services/registry.py +2 -0
- edsl/jobs/Jobs.py +109 -18
- edsl/jobs/buckets/BucketCollection.py +24 -15
- edsl/jobs/buckets/TokenBucket.py +64 -10
- edsl/jobs/interviews/Interview.py +130 -49
- edsl/jobs/interviews/{interview_exception_tracking.py → InterviewExceptionCollection.py} +16 -0
- edsl/jobs/interviews/InterviewExceptionEntry.py +2 -0
- edsl/jobs/runners/JobsRunnerAsyncio.py +119 -173
- edsl/jobs/runners/JobsRunnerStatus.py +332 -0
- edsl/jobs/tasks/QuestionTaskCreator.py +1 -13
- edsl/jobs/tasks/TaskHistory.py +17 -0
- edsl/language_models/LanguageModel.py +36 -38
- edsl/language_models/registry.py +13 -9
- edsl/language_models/utilities.py +5 -2
- edsl/questions/QuestionBase.py +74 -16
- edsl/questions/QuestionBaseGenMixin.py +28 -0
- edsl/questions/QuestionBudget.py +93 -41
- edsl/questions/QuestionCheckBox.py +1 -1
- edsl/questions/QuestionFreeText.py +6 -0
- edsl/questions/QuestionMultipleChoice.py +13 -24
- edsl/questions/QuestionNumerical.py +5 -4
- edsl/questions/Quick.py +41 -0
- edsl/questions/ResponseValidatorABC.py +11 -6
- edsl/questions/derived/QuestionLinearScale.py +4 -1
- edsl/questions/derived/QuestionTopK.py +4 -1
- edsl/questions/derived/QuestionYesNo.py +8 -2
- edsl/questions/descriptors.py +12 -11
- edsl/questions/templates/budget/__init__.py +0 -0
- edsl/questions/templates/budget/answering_instructions.jinja +7 -0
- edsl/questions/templates/budget/question_presentation.jinja +7 -0
- edsl/questions/templates/extract/__init__.py +0 -0
- edsl/questions/templates/numerical/answering_instructions.jinja +0 -1
- edsl/questions/templates/rank/__init__.py +0 -0
- edsl/questions/templates/yes_no/answering_instructions.jinja +2 -2
- edsl/results/DatasetExportMixin.py +5 -1
- edsl/results/Result.py +1 -1
- edsl/results/Results.py +4 -1
- edsl/scenarios/FileStore.py +178 -34
- edsl/scenarios/Scenario.py +76 -37
- edsl/scenarios/ScenarioList.py +19 -2
- edsl/scenarios/ScenarioListPdfMixin.py +150 -4
- edsl/study/Study.py +32 -0
- edsl/surveys/DAG.py +62 -0
- edsl/surveys/MemoryPlan.py +26 -0
- edsl/surveys/Rule.py +34 -1
- edsl/surveys/RuleCollection.py +55 -5
- edsl/surveys/Survey.py +189 -10
- edsl/surveys/base.py +4 -0
- edsl/templates/error_reporting/interview_details.html +6 -1
- edsl/utilities/utilities.py +9 -1
- {edsl-0.1.33.dev2.dist-info → edsl-0.1.34.dist-info}/METADATA +3 -1
- {edsl-0.1.33.dev2.dist-info → edsl-0.1.34.dist-info}/RECORD +75 -69
- edsl/jobs/interviews/retry_management.py +0 -39
- edsl/jobs/runners/JobsRunnerStatusMixin.py +0 -333
- edsl/scenarios/ScenarioImageMixin.py +0 -100
- {edsl-0.1.33.dev2.dist-info → edsl-0.1.34.dist-info}/LICENSE +0 -0
- {edsl-0.1.33.dev2.dist-info → edsl-0.1.34.dist-info}/WHEEL +0 -0
@@ -19,6 +19,7 @@ class QuestionYesNo(QuestionMultipleChoice):
|
|
19
19
|
question_options: list[str] = ["No", "Yes"],
|
20
20
|
answering_instructions: Optional[str] = None,
|
21
21
|
question_presentation: Optional[str] = None,
|
22
|
+
include_comment: Optional[bool] = True,
|
22
23
|
):
|
23
24
|
"""Instantiate a new QuestionYesNo.
|
24
25
|
|
@@ -33,6 +34,7 @@ class QuestionYesNo(QuestionMultipleChoice):
|
|
33
34
|
use_code=False,
|
34
35
|
answering_instructions=answering_instructions,
|
35
36
|
question_presentation=question_presentation,
|
37
|
+
include_comment=include_comment,
|
36
38
|
)
|
37
39
|
self.question_options = question_options
|
38
40
|
|
@@ -41,9 +43,13 @@ class QuestionYesNo(QuestionMultipleChoice):
|
|
41
43
|
################
|
42
44
|
@classmethod
|
43
45
|
@inject_exception
|
44
|
-
def example(cls) -> QuestionYesNo:
|
46
|
+
def example(cls, include_comment: bool = True) -> QuestionYesNo:
|
45
47
|
"""Return an example of a yes/no question."""
|
46
|
-
return cls(
|
48
|
+
return cls(
|
49
|
+
question_name="is_it_equal",
|
50
|
+
question_text="Is 5 + 5 equal to 11?",
|
51
|
+
include_comment=include_comment,
|
52
|
+
)
|
47
53
|
|
48
54
|
|
49
55
|
def main():
|
edsl/questions/descriptors.py
CHANGED
@@ -303,7 +303,7 @@ class QuestionOptionsDescriptor(BaseDescriptor):
|
|
303
303
|
return None
|
304
304
|
else:
|
305
305
|
raise QuestionCreationValidationError(
|
306
|
-
f"Dynamic question options must have
|
306
|
+
f"Dynamic question options must have jinja2 braces - instead received: {value}."
|
307
307
|
)
|
308
308
|
if not isinstance(value, list):
|
309
309
|
raise QuestionCreationValidationError(
|
@@ -325,14 +325,15 @@ class QuestionOptionsDescriptor(BaseDescriptor):
|
|
325
325
|
)
|
326
326
|
if not self.linear_scale:
|
327
327
|
if not self.q_budget:
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
328
|
+
pass
|
329
|
+
# if not (
|
330
|
+
# value
|
331
|
+
# and all(type(x) == type(value[0]) for x in value)
|
332
|
+
# and isinstance(value[0], (str, list, int, float))
|
333
|
+
# ):
|
334
|
+
# raise QuestionCreationValidationError(
|
335
|
+
# f"Question options must be all same type (got {value}).)"
|
336
|
+
# )
|
336
337
|
else:
|
337
338
|
if not all(isinstance(x, (str)) for x in value):
|
338
339
|
raise QuestionCreationValidationError(
|
@@ -390,8 +391,8 @@ class QuestionTextDescriptor(BaseDescriptor):
|
|
390
391
|
|
391
392
|
def validate(self, value, instance):
|
392
393
|
"""Validate the value is a string."""
|
393
|
-
if len(value) > Settings.MAX_QUESTION_LENGTH:
|
394
|
-
|
394
|
+
# if len(value) > Settings.MAX_QUESTION_LENGTH:
|
395
|
+
# raise Exception("Question is too long!")
|
395
396
|
if len(value) < 1:
|
396
397
|
raise Exception("Question is too short!")
|
397
398
|
if not isinstance(value, str):
|
File without changes
|
@@ -0,0 +1,7 @@
|
|
1
|
+
Return only a comma-separated list the values in the same order as the options, with 0s included, on one line, in square braces.
|
2
|
+
|
3
|
+
Example: if there are 4 options, the response should be "[25,25,25,25]" to allocate 25 to each option.
|
4
|
+
|
5
|
+
{% if include_comment %}
|
6
|
+
After the answer, you can put a comment explaining your choice on the next line.
|
7
|
+
{% endif %}
|
File without changes
|
@@ -1,7 +1,6 @@
|
|
1
1
|
This question requires a numerical response in the form of an integer or decimal (e.g., -12, 0, 1, 2, 3.45, ...).
|
2
2
|
Respond with just your number on a single line.
|
3
3
|
If your response is equivalent to zero, report '0'
|
4
|
-
If you cannot determine the answer, report 'None'
|
5
4
|
|
6
5
|
{% if include_comment %}
|
7
6
|
After the answer, put a comment explaining your choice on the next line.
|
File without changes
|
@@ -1,6 +1,6 @@
|
|
1
1
|
{# Answering Instructions #}
|
2
|
-
Please
|
2
|
+
Please respond with just your answer.
|
3
3
|
|
4
4
|
{% if include_comment %}
|
5
|
-
After the answer, you can put a comment explaining your
|
5
|
+
After the answer, you can put a comment explaining your response.
|
6
6
|
{% endif %}
|
@@ -472,7 +472,11 @@ class DatasetExportMixin:
|
|
472
472
|
from edsl import ScenarioList, Scenario
|
473
473
|
|
474
474
|
list_of_dicts = self.to_dicts(remove_prefix=remove_prefix)
|
475
|
-
|
475
|
+
scenarios = []
|
476
|
+
for d in list_of_dicts:
|
477
|
+
scenarios.append(Scenario(d))
|
478
|
+
return ScenarioList(scenarios)
|
479
|
+
# return ScenarioList([Scenario(d) for d in list_of_dicts])
|
476
480
|
|
477
481
|
def to_agent_list(self, remove_prefix: bool = True):
|
478
482
|
"""Convert the results to a list of dictionaries, one per agent.
|
edsl/results/Result.py
CHANGED
@@ -367,7 +367,7 @@ class Result(Base, UserDict):
|
|
367
367
|
"raw_model_response", {"raw_model_response": "No raw model response"}
|
368
368
|
),
|
369
369
|
question_to_attributes=json_dict.get("question_to_attributes", None),
|
370
|
-
generated_tokens=json_dict.get("generated_tokens",
|
370
|
+
generated_tokens=json_dict.get("generated_tokens", {}),
|
371
371
|
)
|
372
372
|
return result
|
373
373
|
|
edsl/results/Results.py
CHANGED
@@ -245,7 +245,9 @@ class Results(UserList, Mixins, Base):
|
|
245
245
|
)
|
246
246
|
|
247
247
|
def __repr__(self) -> str:
|
248
|
-
|
248
|
+
import reprlib
|
249
|
+
|
250
|
+
return f"Results(data = {reprlib.repr(self.data)}, survey = {repr(self.survey)}, created_columns = {self.created_columns})"
|
249
251
|
|
250
252
|
def _repr_html_(self) -> str:
|
251
253
|
from IPython.display import HTML
|
@@ -1089,6 +1091,7 @@ class Results(UserList, Mixins, Base):
|
|
1089
1091
|
stop_on_exception=True,
|
1090
1092
|
skip_retry=True,
|
1091
1093
|
raise_validation_errors=True,
|
1094
|
+
disable_remote_inference=True,
|
1092
1095
|
)
|
1093
1096
|
return results
|
1094
1097
|
|
edsl/scenarios/FileStore.py
CHANGED
@@ -1,41 +1,101 @@
|
|
1
|
-
from edsl import Scenario
|
2
1
|
import base64
|
3
2
|
import io
|
4
3
|
import tempfile
|
5
|
-
|
4
|
+
import mimetypes
|
5
|
+
import os
|
6
|
+
from typing import Dict, Any, IO, Optional
|
7
|
+
import requests
|
8
|
+
from urllib.parse import urlparse
|
9
|
+
|
10
|
+
import google.generativeai as genai
|
11
|
+
|
12
|
+
from edsl import Scenario
|
13
|
+
from edsl.utilities.decorators import add_edsl_version, remove_edsl_version
|
14
|
+
from edsl.utilities.utilities import is_notebook
|
15
|
+
|
16
|
+
|
17
|
+
def view_pdf(pdf_path):
|
18
|
+
import os
|
19
|
+
import subprocess
|
20
|
+
|
21
|
+
if is_notebook():
|
22
|
+
from IPython.display import IFrame
|
23
|
+
from IPython.display import display, HTML
|
24
|
+
|
25
|
+
# Replace 'path/to/your/file.pdf' with the actual path to your PDF file
|
26
|
+
IFrame(pdf_path, width=700, height=600)
|
27
|
+
display(HTML(f'<a href="{pdf_path}" target="_blank">Open PDF</a>'))
|
28
|
+
return
|
29
|
+
|
30
|
+
if os.path.exists(pdf_path):
|
31
|
+
try:
|
32
|
+
if (os_name := os.name) == "posix":
|
33
|
+
# for cool kids
|
34
|
+
subprocess.run(["open", pdf_path], check=True) # macOS
|
35
|
+
elif os_name == "nt":
|
36
|
+
os.startfile(pdf_path) # Windows
|
37
|
+
else:
|
38
|
+
subprocess.run(["xdg-open", pdf_path], check=True) # Linux
|
39
|
+
except Exception as e:
|
40
|
+
print(f"Error opening PDF: {e}")
|
41
|
+
else:
|
42
|
+
print("PDF file was not created successfully.")
|
6
43
|
|
7
44
|
|
8
45
|
class FileStore(Scenario):
|
9
46
|
def __init__(
|
10
47
|
self,
|
11
|
-
|
48
|
+
path: Optional[str] = None,
|
49
|
+
mime_type: Optional[str] = None,
|
12
50
|
binary: Optional[bool] = None,
|
13
51
|
suffix: Optional[str] = None,
|
14
52
|
base64_string: Optional[str] = None,
|
53
|
+
external_locations: Optional[Dict[str, str]] = None,
|
54
|
+
**kwargs,
|
15
55
|
):
|
16
|
-
|
17
|
-
|
56
|
+
if path is None and "filename" in kwargs:
|
57
|
+
path = kwargs["filename"]
|
58
|
+
self.path = path
|
59
|
+
self.suffix = suffix or path.split(".")[-1]
|
18
60
|
self.binary = binary or False
|
19
|
-
self.
|
20
|
-
|
61
|
+
self.mime_type = (
|
62
|
+
mime_type or mimetypes.guess_type(path)[0] or "application/octet-stream"
|
21
63
|
)
|
64
|
+
self.base64_string = base64_string or self.encode_file_to_base64_string(path)
|
65
|
+
self.external_locations = external_locations or {}
|
22
66
|
super().__init__(
|
23
67
|
{
|
24
|
-
"
|
68
|
+
"path": self.path,
|
25
69
|
"base64_string": self.base64_string,
|
26
70
|
"binary": self.binary,
|
27
71
|
"suffix": self.suffix,
|
72
|
+
"mime_type": self.mime_type,
|
73
|
+
"external_locations": self.external_locations,
|
28
74
|
}
|
29
75
|
)
|
30
76
|
|
77
|
+
def __str__(self):
|
78
|
+
return "FileStore: self.path"
|
79
|
+
|
80
|
+
@property
|
81
|
+
def size(self) -> int:
|
82
|
+
return os.path.getsize(self.path)
|
83
|
+
|
84
|
+
def upload_google(self, refresh: bool = False) -> None:
|
85
|
+
genai.configure(api_key=os.getenv("GOOGLE_API_KEY"))
|
86
|
+
google_info = genai.upload_file(self.path, mime_type=self.mime_type)
|
87
|
+
self.external_locations["google"] = google_info.to_dict()
|
88
|
+
|
31
89
|
@classmethod
|
90
|
+
@remove_edsl_version
|
32
91
|
def from_dict(cls, d):
|
33
|
-
return cls(d["filename"], d["binary"], d["suffix"], d["base64_string"])
|
92
|
+
# return cls(d["filename"], d["binary"], d["suffix"], d["base64_string"])
|
93
|
+
return cls(**d)
|
34
94
|
|
35
95
|
def __repr__(self):
|
36
|
-
return f"FileStore(
|
96
|
+
return f"FileStore({self.path})"
|
37
97
|
|
38
|
-
def encode_file_to_base64_string(self, file_path):
|
98
|
+
def encode_file_to_base64_string(self, file_path: str):
|
39
99
|
try:
|
40
100
|
# Attempt to open the file in text mode
|
41
101
|
with open(file_path, "r") as text_file:
|
@@ -56,14 +116,14 @@ class FileStore(Scenario):
|
|
56
116
|
|
57
117
|
return base64_string
|
58
118
|
|
59
|
-
def open(self):
|
119
|
+
def open(self) -> "IO":
|
60
120
|
if self.binary:
|
61
121
|
return self.base64_to_file(self["base64_string"], is_binary=True)
|
62
122
|
else:
|
63
123
|
return self.base64_to_text_file(self["base64_string"])
|
64
124
|
|
65
125
|
@staticmethod
|
66
|
-
def base64_to_text_file(base64_string):
|
126
|
+
def base64_to_text_file(base64_string) -> "IO":
|
67
127
|
# Decode the base64 string to bytes
|
68
128
|
text_data_bytes = base64.b64decode(base64_string)
|
69
129
|
|
@@ -101,7 +161,9 @@ class FileStore(Scenario):
|
|
101
161
|
|
102
162
|
# Create a named temporary file
|
103
163
|
mode = "wb" if self.binary else "w"
|
104
|
-
temp_file = tempfile.NamedTemporaryFile(
|
164
|
+
temp_file = tempfile.NamedTemporaryFile(
|
165
|
+
delete=False, suffix="." + suffix, mode=mode
|
166
|
+
)
|
105
167
|
|
106
168
|
if self.binary:
|
107
169
|
temp_file.write(file_like_object.read())
|
@@ -112,23 +174,95 @@ class FileStore(Scenario):
|
|
112
174
|
|
113
175
|
return temp_file.name
|
114
176
|
|
115
|
-
def
|
177
|
+
def view(self, max_size: int = 300) -> None:
|
178
|
+
if self.suffix == "pdf":
|
179
|
+
view_pdf(self.path)
|
180
|
+
|
181
|
+
if self.suffix == "png" or self.suffix == "jpg" or self.suffix == "jpeg":
|
182
|
+
if is_notebook():
|
183
|
+
from IPython.display import Image
|
184
|
+
from PIL import Image as PILImage
|
185
|
+
|
186
|
+
if max_size:
|
187
|
+
# Open the image using Pillow
|
188
|
+
with PILImage.open(self.path) as img:
|
189
|
+
# Get original width and height
|
190
|
+
original_width, original_height = img.size
|
191
|
+
|
192
|
+
# Calculate the scaling factor
|
193
|
+
scale = min(
|
194
|
+
max_size / original_width, max_size / original_height
|
195
|
+
)
|
196
|
+
|
197
|
+
# Calculate new dimensions
|
198
|
+
new_width = int(original_width * scale)
|
199
|
+
new_height = int(original_height * scale)
|
200
|
+
|
201
|
+
return Image(self.path, width=new_width, height=new_height)
|
202
|
+
else:
|
203
|
+
return Image(self.path)
|
204
|
+
|
205
|
+
def push(
|
206
|
+
self, description: Optional[str] = None, visibility: str = "unlisted"
|
207
|
+
) -> dict:
|
208
|
+
"""
|
209
|
+
Push the object to Coop.
|
210
|
+
:param description: The description of the object to push.
|
211
|
+
:param visibility: The visibility of the object to push.
|
212
|
+
"""
|
116
213
|
scenario_version = Scenario.from_dict(self.to_dict())
|
117
214
|
if description is None:
|
118
|
-
description = "File: " + self
|
119
|
-
info = scenario_version.push(description=description)
|
215
|
+
description = "File: " + self.path
|
216
|
+
info = scenario_version.push(description=description, visibility=visibility)
|
120
217
|
return info
|
121
218
|
|
122
219
|
@classmethod
|
123
|
-
def pull(cls, uuid):
|
124
|
-
|
220
|
+
def pull(cls, uuid: str, expected_parrot_url: Optional[str] = None) -> "FileStore":
|
221
|
+
"""
|
222
|
+
:param uuid: The UUID of the object to pull.
|
223
|
+
:param expected_parrot_url: The URL of the Parrot server to use.
|
224
|
+
:return: The object pulled from the Parrot server.
|
225
|
+
"""
|
226
|
+
scenario_version = Scenario.pull(uuid, expected_parrot_url=expected_parrot_url)
|
125
227
|
return cls.from_dict(scenario_version.to_dict())
|
126
228
|
|
229
|
+
@classmethod
|
230
|
+
def from_url(
|
231
|
+
cls,
|
232
|
+
url: str,
|
233
|
+
download_path: Optional[str] = None,
|
234
|
+
mime_type: Optional[str] = None,
|
235
|
+
) -> "FileStore":
|
236
|
+
"""
|
237
|
+
:param url: The URL of the file to download.
|
238
|
+
:param download_path: The path to save the downloaded file.
|
239
|
+
:param mime_type: The MIME type of the file. If None, it will be guessed from the file extension.
|
240
|
+
"""
|
241
|
+
|
242
|
+
response = requests.get(url, stream=True)
|
243
|
+
response.raise_for_status() # Raises an HTTPError for bad responses
|
244
|
+
|
245
|
+
# Get the filename from the URL if download_path is not provided
|
246
|
+
if download_path is None:
|
247
|
+
filename = os.path.basename(urlparse(url).path)
|
248
|
+
if not filename:
|
249
|
+
filename = "downloaded_file"
|
250
|
+
# download_path = filename
|
251
|
+
download_path = os.path.join(os.getcwd(), filename)
|
252
|
+
|
253
|
+
# Ensure the directory exists
|
254
|
+
os.makedirs(os.path.dirname(download_path), exist_ok=True)
|
255
|
+
|
256
|
+
# Write the file
|
257
|
+
with open(download_path, "wb") as file:
|
258
|
+
for chunk in response.iter_content(chunk_size=8192):
|
259
|
+
file.write(chunk)
|
260
|
+
|
261
|
+
# Create and return a new File instance
|
262
|
+
return cls(download_path, mime_type=mime_type)
|
127
263
|
|
128
|
-
class CSVFileStore(FileStore):
|
129
|
-
def __init__(self, filename):
|
130
|
-
super().__init__(filename, suffix=".csv")
|
131
264
|
|
265
|
+
class CSVFileStore(FileStore):
|
132
266
|
@classmethod
|
133
267
|
def example(cls):
|
134
268
|
from edsl.results.Results import Results
|
@@ -147,9 +281,6 @@ class CSVFileStore(FileStore):
|
|
147
281
|
|
148
282
|
|
149
283
|
class PDFFileStore(FileStore):
|
150
|
-
def __init__(self, filename):
|
151
|
-
super().__init__(filename, suffix=".pdf")
|
152
|
-
|
153
284
|
def view(self):
|
154
285
|
pdf_path = self.to_tempfile()
|
155
286
|
print(f"PDF path: {pdf_path}") # Print the path to ensure it exists
|
@@ -225,9 +356,6 @@ class PDFFileStore(FileStore):
|
|
225
356
|
|
226
357
|
|
227
358
|
class PNGFileStore(FileStore):
|
228
|
-
def __init__(self, filename):
|
229
|
-
super().__init__(filename, suffix=".png")
|
230
|
-
|
231
359
|
@classmethod
|
232
360
|
def example(cls):
|
233
361
|
import textwrap
|
@@ -251,9 +379,6 @@ class PNGFileStore(FileStore):
|
|
251
379
|
|
252
380
|
|
253
381
|
class SQLiteFileStore(FileStore):
|
254
|
-
def __init__(self, filename):
|
255
|
-
super().__init__(filename, suffix=".sqlite")
|
256
|
-
|
257
382
|
@classmethod
|
258
383
|
def example(cls):
|
259
384
|
import sqlite3
|
@@ -265,6 +390,8 @@ class SQLiteFileStore(FileStore):
|
|
265
390
|
c.execute("""CREATE TABLE stocks (date text)""")
|
266
391
|
conn.commit()
|
267
392
|
|
393
|
+
return cls(f.name)
|
394
|
+
|
268
395
|
def view(self):
|
269
396
|
import subprocess
|
270
397
|
import os
|
@@ -273,6 +400,22 @@ class SQLiteFileStore(FileStore):
|
|
273
400
|
os.system(f"sqlite3 {sqlite_path}")
|
274
401
|
|
275
402
|
|
403
|
+
class HTMLFileStore(FileStore):
|
404
|
+
@classmethod
|
405
|
+
def example(cls):
|
406
|
+
import tempfile
|
407
|
+
|
408
|
+
with tempfile.NamedTemporaryFile(suffix=".html", delete=False) as f:
|
409
|
+
f.write("<html><body><h1>Test</h1></body></html>".encode())
|
410
|
+
return cls(f.name)
|
411
|
+
|
412
|
+
def view(self):
|
413
|
+
import webbrowser
|
414
|
+
|
415
|
+
html_path = self.to_tempfile()
|
416
|
+
webbrowser.open("file://" + html_path)
|
417
|
+
|
418
|
+
|
276
419
|
if __name__ == "__main__":
|
277
420
|
# file_path = "../conjure/examples/Ex11-2.sav"
|
278
421
|
# fs = FileStore(file_path)
|
@@ -289,9 +432,10 @@ if __name__ == "__main__":
|
|
289
432
|
# fs = PDFFileStore("paper.pdf")
|
290
433
|
# fs.view()
|
291
434
|
# from edsl import Conjure
|
292
|
-
|
293
|
-
fs = PNGFileStore("
|
294
|
-
fs.view()
|
435
|
+
pass
|
436
|
+
# fs = PNGFileStore("logo.png")
|
437
|
+
# fs.view()
|
438
|
+
# fs.upload_google()
|
295
439
|
|
296
440
|
# c = Conjure(datafile_name=fs.to_tempfile())
|
297
441
|
# f = PDFFileStore("paper.pdf")
|
edsl/scenarios/Scenario.py
CHANGED
@@ -2,19 +2,18 @@
|
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
import copy
|
5
|
-
import base64
|
6
5
|
import hashlib
|
7
6
|
import os
|
8
7
|
from collections import UserDict
|
9
8
|
from typing import Union, List, Optional, Generator
|
10
9
|
from uuid import uuid4
|
10
|
+
|
11
11
|
from edsl.Base import Base
|
12
|
-
from edsl.scenarios.ScenarioImageMixin import ScenarioImageMixin
|
13
12
|
from edsl.scenarios.ScenarioHtmlMixin import ScenarioHtmlMixin
|
14
13
|
from edsl.utilities.decorators import add_edsl_version, remove_edsl_version
|
15
14
|
|
16
15
|
|
17
|
-
class Scenario(Base, UserDict,
|
16
|
+
class Scenario(Base, UserDict, ScenarioHtmlMixin):
|
18
17
|
"""A Scenario is a dictionary of keys/values.
|
19
18
|
|
20
19
|
They can be used parameterize edsl questions."""
|
@@ -42,16 +41,46 @@ class Scenario(Base, UserDict, ScenarioImageMixin, ScenarioHtmlMixin):
|
|
42
41
|
|
43
42
|
return ScenarioList([copy.deepcopy(self) for _ in range(n)])
|
44
43
|
|
44
|
+
# @property
|
45
|
+
# def has_image(self) -> bool:
|
46
|
+
# """Return whether the scenario has an image."""
|
47
|
+
# if not hasattr(self, "_has_image"):
|
48
|
+
# self._has_image = False
|
49
|
+
# return self._has_image
|
50
|
+
|
45
51
|
@property
|
46
|
-
def
|
47
|
-
"""Return whether the scenario has
|
48
|
-
|
49
|
-
|
50
|
-
|
52
|
+
def has_jinja_braces(self) -> bool:
|
53
|
+
"""Return whether the scenario has jinja braces. This matters for rendering.
|
54
|
+
|
55
|
+
>>> s = Scenario({"food": "I love {{wood chips}}"})
|
56
|
+
>>> s.has_jinja_braces
|
57
|
+
True
|
58
|
+
"""
|
59
|
+
for _, value in self.items():
|
60
|
+
if isinstance(value, str):
|
61
|
+
if "{{" in value and "}}" in value:
|
62
|
+
return True
|
63
|
+
return False
|
51
64
|
|
52
|
-
|
53
|
-
|
54
|
-
|
65
|
+
def convert_jinja_braces(
|
66
|
+
self, replacement_left="<<", replacement_right=">>"
|
67
|
+
) -> Scenario:
|
68
|
+
"""Convert Jinja braces to some other character.
|
69
|
+
|
70
|
+
>>> s = Scenario({"food": "I love {{wood chips}}"})
|
71
|
+
>>> s.convert_jinja_braces()
|
72
|
+
Scenario({'food': 'I love <<wood chips>>'})
|
73
|
+
|
74
|
+
"""
|
75
|
+
new_scenario = Scenario()
|
76
|
+
for key, value in self.items():
|
77
|
+
if isinstance(value, str):
|
78
|
+
new_scenario[key] = value.replace("{{", replacement_left).replace(
|
79
|
+
"}}", replacement_right
|
80
|
+
)
|
81
|
+
else:
|
82
|
+
new_scenario[key] = value
|
83
|
+
return new_scenario
|
55
84
|
|
56
85
|
def __add__(self, other_scenario: "Scenario") -> "Scenario":
|
57
86
|
"""Combine two scenarios by taking the union of their keys
|
@@ -75,8 +104,6 @@ class Scenario(Base, UserDict, ScenarioImageMixin, ScenarioHtmlMixin):
|
|
75
104
|
data1 = copy.deepcopy(self.data)
|
76
105
|
data2 = copy.deepcopy(other_scenario.data)
|
77
106
|
s = Scenario(data1 | data2)
|
78
|
-
if self.has_image or other_scenario.has_image:
|
79
|
-
s._has_image = True
|
80
107
|
return s
|
81
108
|
|
82
109
|
def rename(self, replacement_dict: dict) -> "Scenario":
|
@@ -142,6 +169,7 @@ class Scenario(Base, UserDict, ScenarioImageMixin, ScenarioHtmlMixin):
|
|
142
169
|
print_json(json.dumps(self.to_dict()))
|
143
170
|
|
144
171
|
def __repr__(self):
|
172
|
+
# return "Scenario(" + reprlib.repr(self.data) + ")"
|
145
173
|
return "Scenario(" + repr(self.data) + ")"
|
146
174
|
|
147
175
|
def _repr_html_(self):
|
@@ -196,26 +224,34 @@ class Scenario(Base, UserDict, ScenarioImageMixin, ScenarioHtmlMixin):
|
|
196
224
|
return cls({"url": url, field_name: text})
|
197
225
|
|
198
226
|
@classmethod
|
199
|
-
def
|
200
|
-
"""Creates a scenario
|
227
|
+
def from_file(cls, file_path: str, field_name: str) -> "Scenario":
|
228
|
+
"""Creates a scenario from a file."""
|
229
|
+
from edsl.scenarios.FileStore import FileStore
|
201
230
|
|
202
|
-
|
231
|
+
fs = FileStore(file_path)
|
232
|
+
return cls({field_name: fs})
|
203
233
|
|
204
|
-
|
205
|
-
|
206
|
-
|
234
|
+
@classmethod
|
235
|
+
def from_image(
|
236
|
+
cls, image_path: str, image_name: Optional[str] = None
|
237
|
+
) -> "Scenario":
|
207
238
|
"""
|
208
|
-
with
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
239
|
+
Creates a scenario with a base64 encoding of an image.
|
240
|
+
|
241
|
+
Args:
|
242
|
+
image_path (str): Path to the image file.
|
243
|
+
|
244
|
+
Returns:
|
245
|
+
Scenario: A new Scenario instance with image information.
|
246
|
+
|
247
|
+
"""
|
248
|
+
if not os.path.exists(image_path):
|
249
|
+
raise FileNotFoundError(f"Image file not found: {image_path}")
|
250
|
+
|
251
|
+
if image_name is None:
|
252
|
+
image_name = os.path.basename(image_path).split(".")[0]
|
253
|
+
|
254
|
+
return cls.from_file(image_path, image_name)
|
219
255
|
|
220
256
|
@classmethod
|
221
257
|
def from_pdf(cls, pdf_path):
|
@@ -429,18 +465,21 @@ class Scenario(Base, UserDict, ScenarioImageMixin, ScenarioHtmlMixin):
|
|
429
465
|
return table
|
430
466
|
|
431
467
|
@classmethod
|
432
|
-
def example(cls, randomize: bool = False) -> Scenario:
|
468
|
+
def example(cls, randomize: bool = False, has_image=False) -> Scenario:
|
433
469
|
"""
|
434
470
|
Returns an example Scenario instance.
|
435
471
|
|
436
472
|
:param randomize: If True, adds a random string to the value of the example key.
|
437
473
|
"""
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
474
|
+
if not has_image:
|
475
|
+
addition = "" if not randomize else str(uuid4())
|
476
|
+
return cls(
|
477
|
+
{
|
478
|
+
"persona": f"A reseacher studying whether LLMs can be used to generate surveys.{addition}",
|
479
|
+
}
|
480
|
+
)
|
481
|
+
else:
|
482
|
+
return cls.from_image(cls.example_image())
|
444
483
|
|
445
484
|
def code(self) -> List[str]:
|
446
485
|
"""Return the code for the scenario."""
|