edsl 0.1.51__py3-none-any.whl → 0.1.53__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/__init__.py +45 -34
- edsl/__version__.py +1 -1
- edsl/conversation/Conversation.py +2 -1
- edsl/coop/coop.py +2 -0
- edsl/interviews/answering_function.py +20 -21
- edsl/interviews/exception_tracking.py +4 -3
- edsl/interviews/interview_task_manager.py +5 -2
- edsl/interviews/request_token_estimator.py +104 -2
- edsl/invigilators/invigilators.py +37 -4
- edsl/jobs/html_table_job_logger.py +494 -257
- edsl/jobs/jobs_status_enums.py +1 -0
- edsl/jobs/remote_inference.py +46 -12
- edsl/language_models/language_model.py +148 -146
- edsl/results/results.py +31 -2
- edsl/scenarios/file_store.py +73 -23
- edsl/tasks/task_history.py +45 -8
- edsl/templates/error_reporting/base.html +37 -4
- edsl/templates/error_reporting/exceptions_table.html +105 -33
- edsl/templates/error_reporting/interview_details.html +130 -126
- edsl/templates/error_reporting/overview.html +21 -25
- edsl/templates/error_reporting/report.css +215 -46
- edsl/templates/error_reporting/report.js +122 -20
- {edsl-0.1.51.dist-info → edsl-0.1.53.dist-info}/METADATA +1 -1
- {edsl-0.1.51.dist-info → edsl-0.1.53.dist-info}/RECORD +27 -27
- {edsl-0.1.51.dist-info → edsl-0.1.53.dist-info}/LICENSE +0 -0
- {edsl-0.1.51.dist-info → edsl-0.1.53.dist-info}/WHEEL +0 -0
- {edsl-0.1.51.dist-info → edsl-0.1.53.dist-info}/entry_points.txt +0 -0
edsl/results/results.py
CHANGED
@@ -44,10 +44,10 @@ from typing import Optional, Callable, Any, Union, List, TYPE_CHECKING
|
|
44
44
|
from bisect import bisect_left
|
45
45
|
|
46
46
|
from ..base import Base
|
47
|
+
from ..caching import Cache, CacheEntry
|
47
48
|
|
48
49
|
if TYPE_CHECKING:
|
49
50
|
from ..surveys import Survey
|
50
|
-
from ..caching import Cache
|
51
51
|
from ..agents import AgentList
|
52
52
|
from ..scenarios import ScenarioList
|
53
53
|
from ..results import Result
|
@@ -707,12 +707,41 @@ class Results(UserList, ResultsOperationsMixin, Base):
|
|
707
707
|
"b_not_a": [other_results[i] for i in indices_other],
|
708
708
|
}
|
709
709
|
|
710
|
+
def initialize_cache_from_results(self):
|
711
|
+
cache = Cache(data={})
|
712
|
+
|
713
|
+
for result in self.data:
|
714
|
+
for key in result.data["prompt"]:
|
715
|
+
if key.endswith("_system_prompt"):
|
716
|
+
question_name = key.removesuffix("_system_prompt")
|
717
|
+
system_prompt = result.data["prompt"][key].text
|
718
|
+
user_key = f"{question_name}_user_prompt"
|
719
|
+
if user_key in result.data["prompt"]:
|
720
|
+
user_prompt = result.data["prompt"][user_key].text
|
721
|
+
else:
|
722
|
+
user_prompt = ""
|
723
|
+
|
724
|
+
# Get corresponding model response
|
725
|
+
response_key = f"{question_name}_raw_model_response"
|
726
|
+
output = result.data["raw_model_response"].get(response_key, "")
|
727
|
+
|
728
|
+
entry = CacheEntry(
|
729
|
+
model=result.model.model,
|
730
|
+
parameters=result.model.parameters,
|
731
|
+
system_prompt=system_prompt,
|
732
|
+
user_prompt=user_prompt,
|
733
|
+
output=json.dumps(output),
|
734
|
+
iteration=0,
|
735
|
+
)
|
736
|
+
cache.data[entry.key] = entry
|
737
|
+
|
738
|
+
self.cache = cache
|
739
|
+
|
710
740
|
@property
|
711
741
|
def has_unfixed_exceptions(self) -> bool:
|
712
742
|
return self.task_history.has_unfixed_exceptions
|
713
743
|
|
714
744
|
def __hash__(self) -> int:
|
715
|
-
|
716
745
|
return dict_hash(
|
717
746
|
self.to_dict(sort=True, add_edsl_version=False, include_cache_info=False)
|
718
747
|
)
|
edsl/scenarios/file_store.py
CHANGED
@@ -17,25 +17,26 @@ from .file_methods import FileMethods
|
|
17
17
|
if TYPE_CHECKING:
|
18
18
|
from .scenario_list import ScenarioList
|
19
19
|
|
20
|
+
|
20
21
|
class FileStore(Scenario):
|
21
22
|
"""
|
22
23
|
A specialized Scenario subclass for managing file content and metadata.
|
23
|
-
|
24
|
+
|
24
25
|
FileStore provides functionality for working with files in EDSL, handling various
|
25
26
|
file formats with appropriate encoding, storage, and access methods. It extends
|
26
27
|
Scenario to allow files to be included in surveys, questions, and other EDSL components.
|
27
|
-
|
28
|
+
|
28
29
|
FileStore supports multiple file formats including text, PDF, Word documents, images,
|
29
30
|
and more. It can load files from local paths or URLs, and provides methods for
|
30
31
|
accessing file content, extracting text, and managing file operations.
|
31
|
-
|
32
|
+
|
32
33
|
Key features:
|
33
34
|
- Base64 encoding for portability and serialization
|
34
35
|
- Lazy loading through temporary files when needed
|
35
36
|
- Automatic MIME type detection
|
36
37
|
- Text extraction from various file formats
|
37
38
|
- Format-specific operations through specialized handlers
|
38
|
-
|
39
|
+
|
39
40
|
Attributes:
|
40
41
|
_path (str): The original file path.
|
41
42
|
_temp_path (str): Path to any generated temporary file.
|
@@ -45,7 +46,7 @@ class FileStore(Scenario):
|
|
45
46
|
base64_string (str): Base64-encoded file content.
|
46
47
|
external_locations (dict): Dictionary of external locations.
|
47
48
|
extracted_text (str): Text extracted from the file.
|
48
|
-
|
49
|
+
|
49
50
|
Examples:
|
50
51
|
>>> import tempfile
|
51
52
|
>>> # Create a text file
|
@@ -53,13 +54,14 @@ class FileStore(Scenario):
|
|
53
54
|
... _ = f.write("Hello World")
|
54
55
|
... _ = f.flush()
|
55
56
|
... fs = FileStore(f.name)
|
56
|
-
|
57
|
+
|
57
58
|
# The following example works locally but is commented out for CI environments
|
58
59
|
# where dependencies like pandoc may not be available:
|
59
60
|
# >>> # FileStore supports various formats
|
60
61
|
# >>> formats = ["txt", "pdf", "docx", "pptx", "md", "py", "json", "csv", "html", "png", "db"]
|
61
62
|
# >>> _ = [FileStore.example(format) for format in formats]
|
62
63
|
"""
|
64
|
+
|
63
65
|
__documentation__ = "https://docs.expectedparrot.com/en/latest/filestore.html"
|
64
66
|
|
65
67
|
def __init__(
|
@@ -75,11 +77,11 @@ class FileStore(Scenario):
|
|
75
77
|
):
|
76
78
|
"""
|
77
79
|
Initialize a new FileStore object.
|
78
|
-
|
80
|
+
|
79
81
|
This constructor creates a FileStore object from either a file path or a base64-encoded
|
80
82
|
string representation of file content. It handles automatic detection of file properties
|
81
83
|
like MIME type, extracts text content when possible, and manages file encoding.
|
82
|
-
|
84
|
+
|
83
85
|
Args:
|
84
86
|
path: Path to the file to load. Can be a local file path or URL.
|
85
87
|
mime_type: MIME type of the file. If not provided, will be auto-detected.
|
@@ -93,7 +95,7 @@ class FileStore(Scenario):
|
|
93
95
|
text will be extracted automatically if possible.
|
94
96
|
**kwargs: Additional keyword arguments. 'filename' can be used as an
|
95
97
|
alternative to 'path'.
|
96
|
-
|
98
|
+
|
97
99
|
Note:
|
98
100
|
If path is a URL (starts with http:// or https://), the file will be
|
99
101
|
downloaded automatically.
|
@@ -138,15 +140,15 @@ class FileStore(Scenario):
|
|
138
140
|
def path(self) -> str:
|
139
141
|
"""
|
140
142
|
Returns a valid path to the file content, creating a temporary file if needed.
|
141
|
-
|
143
|
+
|
142
144
|
This property ensures that a valid file path is always available for the file
|
143
145
|
content, even if the original file is no longer accessible or if the FileStore
|
144
146
|
was created from a base64 string without a path. If the original path doesn't
|
145
147
|
exist, it automatically generates a temporary file from the base64 content.
|
146
|
-
|
148
|
+
|
147
149
|
Returns:
|
148
150
|
A string containing a valid file path to access the file content.
|
149
|
-
|
151
|
+
|
150
152
|
Examples:
|
151
153
|
>>> import tempfile, os
|
152
154
|
>>> with tempfile.NamedTemporaryFile(suffix=".txt", mode="w") as f:
|
@@ -155,8 +157,8 @@ class FileStore(Scenario):
|
|
155
157
|
... fs = FileStore(f.name)
|
156
158
|
... os.path.isfile(fs.path)
|
157
159
|
True
|
158
|
-
|
159
|
-
|
160
|
+
|
161
|
+
|
160
162
|
Notes:
|
161
163
|
- The path may point to a temporary file that will be cleaned up when the
|
162
164
|
Python process exits
|
@@ -319,9 +321,10 @@ class FileStore(Scenario):
|
|
319
321
|
|
320
322
|
link = ConstructDownloadLink(self).html_create_link(self.path, style=None)
|
321
323
|
return f"{parent_html}<br>{link}"
|
322
|
-
|
324
|
+
|
323
325
|
def download_link(self):
|
324
326
|
from .construct_download_link import ConstructDownloadLink
|
327
|
+
|
325
328
|
return ConstructDownloadLink(self).html_create_link(self.path, style=None)
|
326
329
|
|
327
330
|
def encode_file_to_base64_string(self, file_path: str):
|
@@ -572,6 +575,53 @@ class FileStore(Scenario):
|
|
572
575
|
f"Converting {self.suffix} files to pandas DataFrame is not supported"
|
573
576
|
)
|
574
577
|
|
578
|
+
def is_image(self) -> bool:
|
579
|
+
"""
|
580
|
+
Check if the file is an image by examining its MIME type.
|
581
|
+
|
582
|
+
Returns:
|
583
|
+
bool: True if the file is an image, False otherwise.
|
584
|
+
|
585
|
+
Examples:
|
586
|
+
>>> fs = FileStore.example("png")
|
587
|
+
>>> fs.is_image()
|
588
|
+
True
|
589
|
+
>>> fs = FileStore.example("txt")
|
590
|
+
>>> fs.is_image()
|
591
|
+
False
|
592
|
+
"""
|
593
|
+
# Check if the mime type starts with 'image/'
|
594
|
+
return self.mime_type.startswith("image/")
|
595
|
+
|
596
|
+
def get_image_dimensions(self) -> tuple:
|
597
|
+
"""
|
598
|
+
Get the dimensions (width, height) of an image file.
|
599
|
+
|
600
|
+
Returns:
|
601
|
+
tuple: A tuple containing the width and height of the image.
|
602
|
+
|
603
|
+
Raises:
|
604
|
+
ValueError: If the file is not an image or PIL is not installed.
|
605
|
+
|
606
|
+
Examples:
|
607
|
+
>>> fs = FileStore.example("png")
|
608
|
+
>>> width, height = fs.get_image_dimensions()
|
609
|
+
>>> isinstance(width, int) and isinstance(height, int)
|
610
|
+
True
|
611
|
+
"""
|
612
|
+
if not self.is_image():
|
613
|
+
raise ValueError("This file is not an image")
|
614
|
+
|
615
|
+
try:
|
616
|
+
from PIL import Image
|
617
|
+
except ImportError:
|
618
|
+
raise ImportError(
|
619
|
+
"PIL (Pillow) is required to get image dimensions. Install it with: pip install pillow"
|
620
|
+
)
|
621
|
+
|
622
|
+
with Image.open(self.path) as img:
|
623
|
+
return img.size # Returns (width, height)
|
624
|
+
|
575
625
|
def __getattr__(self, name):
|
576
626
|
"""
|
577
627
|
Delegate pandas DataFrame methods to the underlying DataFrame if this is a CSV file
|
@@ -662,13 +712,13 @@ class FileStore(Scenario):
|
|
662
712
|
# endobj
|
663
713
|
# xref
|
664
714
|
# 0 7
|
665
|
-
# 0000000000 65535 f
|
666
|
-
# 0000000010 00000 n
|
667
|
-
# 0000000053 00000 n
|
668
|
-
# 0000000100 00000 n
|
669
|
-
# 0000000173 00000 n
|
670
|
-
# 0000000232 00000 n
|
671
|
-
# 0000000272 00000 n
|
715
|
+
# 0000000000 65535 f
|
716
|
+
# 0000000010 00000 n
|
717
|
+
# 0000000053 00000 n
|
718
|
+
# 0000000100 00000 n
|
719
|
+
# 0000000173 00000 n
|
720
|
+
# 0000000232 00000 n
|
721
|
+
# 0000000272 00000 n
|
672
722
|
# trailer
|
673
723
|
# << /Size 7 /Root 1 0 R >>
|
674
724
|
# startxref
|
@@ -748,6 +798,7 @@ class FileStore(Scenario):
|
|
748
798
|
|
749
799
|
if __name__ == "__main__":
|
750
800
|
import doctest
|
801
|
+
|
751
802
|
doctest.testmod()
|
752
803
|
|
753
804
|
# formats = FileMethods.supported_file_types()
|
@@ -756,4 +807,3 @@ if __name__ == "__main__":
|
|
756
807
|
# fs = FileStore.example(file_type)
|
757
808
|
# fs.view()
|
758
809
|
# input("Press Enter to continue...")
|
759
|
-
|
edsl/tasks/task_history.py
CHANGED
@@ -302,22 +302,59 @@ class TaskHistory(RepresentationMixin):
|
|
302
302
|
js = env.joinpath("report.js").read_text()
|
303
303
|
return js
|
304
304
|
|
305
|
+
# @property
|
306
|
+
# def exceptions_table(self) -> dict:
|
307
|
+
# """Return a dictionary of exceptions organized by type, service, model, and question name."""
|
308
|
+
# exceptions_table = {}
|
309
|
+
# for interview in self.total_interviews:
|
310
|
+
# for question_name, exceptions in interview.exceptions.items():
|
311
|
+
# for exception in exceptions:
|
312
|
+
# key = (
|
313
|
+
# exception.exception.__class__.__name__, # Exception type
|
314
|
+
# interview.model._inference_service_, # Service
|
315
|
+
# interview.model.model, # Model
|
316
|
+
# question_name, # Question name
|
317
|
+
# )
|
318
|
+
# if key not in exceptions_table:
|
319
|
+
# exceptions_table[key] = 0
|
320
|
+
# exceptions_table[key] += 1
|
321
|
+
# return exceptions_table
|
322
|
+
|
305
323
|
@property
|
306
324
|
def exceptions_table(self) -> dict:
|
307
|
-
"""Return a dictionary of exceptions organized by type, service, model, and question name."""
|
325
|
+
"""Return a dictionary of unique exceptions organized by type, service, model, and question name."""
|
308
326
|
exceptions_table = {}
|
327
|
+
seen_exceptions = set()
|
328
|
+
|
309
329
|
for interview in self.total_interviews:
|
310
330
|
for question_name, exceptions in interview.exceptions.items():
|
311
331
|
for exception in exceptions:
|
312
|
-
|
332
|
+
# Create a unique identifier for this exception based on its content
|
333
|
+
exception_key = (
|
313
334
|
exception.exception.__class__.__name__, # Exception type
|
314
|
-
interview.model._inference_service_,
|
315
|
-
interview.model.model,
|
316
|
-
question_name,
|
335
|
+
interview.model._inference_service_, # Service
|
336
|
+
interview.model.model, # Model
|
337
|
+
question_name, # Question name
|
338
|
+
exception.name, # Exception name
|
339
|
+
str(exception.traceback)[:100] if exception.traceback else "", # Truncated traceback
|
317
340
|
)
|
318
|
-
|
319
|
-
|
320
|
-
|
341
|
+
|
342
|
+
# Only count if we haven't seen this exact exception before
|
343
|
+
if exception_key not in seen_exceptions:
|
344
|
+
seen_exceptions.add(exception_key)
|
345
|
+
|
346
|
+
# Add to the summary table
|
347
|
+
table_key = (
|
348
|
+
exception.exception.__class__.__name__, # Exception type
|
349
|
+
interview.model._inference_service_, # Service
|
350
|
+
interview.model.model, # Model
|
351
|
+
question_name, # Question name
|
352
|
+
)
|
353
|
+
|
354
|
+
if table_key not in exceptions_table:
|
355
|
+
exceptions_table[table_key] = 0
|
356
|
+
exceptions_table[table_key] += 1
|
357
|
+
|
321
358
|
return exceptions_table
|
322
359
|
|
323
360
|
@property
|
@@ -5,6 +5,39 @@
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6
6
|
<title>Exceptions Report</title>
|
7
7
|
<style>
|
8
|
+
/* Global styles */
|
9
|
+
:root {
|
10
|
+
--primary-color: #3f51b5;
|
11
|
+
--secondary-color: #5c6bc0;
|
12
|
+
--success-color: #4caf50;
|
13
|
+
--error-color: #f44336;
|
14
|
+
--warning-color: #ff9800;
|
15
|
+
--text-color: #333;
|
16
|
+
--light-bg: #f5f7fa;
|
17
|
+
--border-color: #e0e0e0;
|
18
|
+
--header-bg: #f9f9f9;
|
19
|
+
--card-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
20
|
+
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
21
|
+
}
|
22
|
+
|
23
|
+
* {
|
24
|
+
box-sizing: border-box;
|
25
|
+
}
|
26
|
+
|
27
|
+
body {
|
28
|
+
font-family: var(--font-family);
|
29
|
+
background-color: var(--light-bg);
|
30
|
+
color: var(--text-color);
|
31
|
+
line-height: 1.6;
|
32
|
+
margin: 0;
|
33
|
+
padding: 20px;
|
34
|
+
}
|
35
|
+
|
36
|
+
.container {
|
37
|
+
max-width: 1200px;
|
38
|
+
margin: 0 auto;
|
39
|
+
}
|
40
|
+
|
8
41
|
{{ css }}
|
9
42
|
</style>
|
10
43
|
|
@@ -14,9 +47,9 @@
|
|
14
47
|
|
15
48
|
</head>
|
16
49
|
<body>
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
50
|
+
<div class="container">
|
51
|
+
{% include 'exceptions_table.html' %}
|
52
|
+
{% include 'interviews.html' %}
|
53
|
+
</div>
|
21
54
|
</body>
|
22
55
|
</html>
|
@@ -1,34 +1,106 @@
|
|
1
|
+
<div class="summary-section">
|
2
|
+
<div class="table-container">
|
3
|
+
<h2>Exceptions Report</h2>
|
4
|
+
<table class="exceptions-table">
|
5
|
+
<thead>
|
6
|
+
<tr>
|
7
|
+
<th>Exception Type</th>
|
8
|
+
<th>Service</th>
|
9
|
+
<th>Model</th>
|
10
|
+
<th>Question Name</th>
|
11
|
+
<th class="count-column">Count</th>
|
12
|
+
</tr>
|
13
|
+
</thead>
|
14
|
+
<tbody>
|
15
|
+
{% for (exception_type, service, model, question_name), count in exceptions_table.items() %}
|
16
|
+
<tr>
|
17
|
+
<td>{{ exception_type }}</td>
|
18
|
+
<td>{{ service }}</td>
|
19
|
+
<td>{{ model }}</td>
|
20
|
+
<td>{{ question_name }}</td>
|
21
|
+
<td class="count-cell">{{ count }}</td>
|
22
|
+
</tr>
|
23
|
+
{% endfor %}
|
24
|
+
</tbody>
|
25
|
+
</table>
|
26
|
+
</div>
|
27
|
+
|
28
|
+
<p class="note">
|
29
|
+
Note: Each unique exception is counted only once. You may encounter repeated exceptions where retries were attempted.
|
30
|
+
</p>
|
31
|
+
</div>
|
32
|
+
|
1
33
|
<style>
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
34
|
+
/* Summary section styles */
|
35
|
+
.summary-section {
|
36
|
+
background-color: white;
|
37
|
+
border-radius: 8px;
|
38
|
+
margin-bottom: 24px;
|
39
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
40
|
+
overflow: hidden;
|
41
|
+
border: 1px solid #e0e0e0;
|
42
|
+
padding: 0 0 16px 0;
|
43
|
+
}
|
44
|
+
|
45
|
+
.section-header {
|
46
|
+
background-color: #f9f9f9;
|
47
|
+
padding: 12px 16px;
|
48
|
+
border-bottom: 1px solid #e0e0e0;
|
49
|
+
}
|
50
|
+
|
51
|
+
.section-header h2 {
|
52
|
+
margin: 0;
|
53
|
+
font-size: 18px;
|
54
|
+
font-weight: 500;
|
55
|
+
color: #3f51b5;
|
56
|
+
}
|
57
|
+
|
58
|
+
.table-container {
|
59
|
+
padding: 16px;
|
60
|
+
overflow-x: auto;
|
61
|
+
}
|
62
|
+
|
63
|
+
/* Table styles */
|
64
|
+
.exceptions-table {
|
65
|
+
width: 100%;
|
66
|
+
border-collapse: collapse;
|
67
|
+
margin-bottom: 16px;
|
68
|
+
}
|
69
|
+
|
70
|
+
.exceptions-table th {
|
71
|
+
background-color: #f5f5f5;
|
72
|
+
color: #333;
|
73
|
+
font-weight: 500;
|
74
|
+
text-align: left;
|
75
|
+
padding: 12px;
|
76
|
+
border-bottom: 2px solid #e0e0e0;
|
77
|
+
}
|
78
|
+
|
79
|
+
.exceptions-table td {
|
80
|
+
padding: 10px 12px;
|
81
|
+
border-bottom: 1px solid #e0e0e0;
|
82
|
+
color: #333;
|
83
|
+
}
|
84
|
+
|
85
|
+
.exceptions-table tr:hover {
|
86
|
+
background-color: #f9f9f9;
|
87
|
+
}
|
88
|
+
|
89
|
+
.count-column {
|
90
|
+
width: 80px;
|
91
|
+
text-align: center;
|
92
|
+
}
|
93
|
+
|
94
|
+
.count-cell {
|
95
|
+
text-align: center;
|
96
|
+
font-weight: 500;
|
97
|
+
}
|
98
|
+
|
99
|
+
/* Note styles */
|
100
|
+
.note {
|
101
|
+
font-size: 14px;
|
102
|
+
color: #666;
|
103
|
+
margin: 0 16px;
|
104
|
+
line-height: 1.5;
|
105
|
+
}
|
106
|
+
</style>
|