edsl 0.1.41__py3-none-any.whl → 0.1.43__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. edsl/__version__.py +1 -1
  2. edsl/agents/Invigilator.py +4 -3
  3. edsl/agents/InvigilatorBase.py +2 -1
  4. edsl/agents/PromptConstructor.py +92 -21
  5. edsl/agents/QuestionInstructionPromptBuilder.py +68 -9
  6. edsl/agents/QuestionTemplateReplacementsBuilder.py +7 -2
  7. edsl/agents/prompt_helpers.py +2 -2
  8. edsl/coop/coop.py +97 -19
  9. edsl/enums.py +3 -1
  10. edsl/exceptions/coop.py +4 -0
  11. edsl/exceptions/jobs.py +1 -9
  12. edsl/exceptions/language_models.py +8 -4
  13. edsl/exceptions/questions.py +8 -11
  14. edsl/inference_services/AvailableModelFetcher.py +4 -1
  15. edsl/inference_services/DeepSeekService.py +18 -0
  16. edsl/inference_services/registry.py +2 -0
  17. edsl/jobs/Jobs.py +60 -34
  18. edsl/jobs/JobsPrompts.py +64 -3
  19. edsl/jobs/JobsRemoteInferenceHandler.py +42 -25
  20. edsl/jobs/JobsRemoteInferenceLogger.py +1 -1
  21. edsl/jobs/buckets/BucketCollection.py +30 -0
  22. edsl/jobs/data_structures.py +1 -0
  23. edsl/jobs/interviews/Interview.py +1 -1
  24. edsl/jobs/loggers/HTMLTableJobLogger.py +6 -1
  25. edsl/jobs/results_exceptions_handler.py +2 -7
  26. edsl/jobs/tasks/TaskHistory.py +49 -17
  27. edsl/language_models/LanguageModel.py +7 -4
  28. edsl/language_models/ModelList.py +1 -1
  29. edsl/language_models/key_management/KeyLookupBuilder.py +47 -20
  30. edsl/language_models/key_management/models.py +10 -4
  31. edsl/language_models/model.py +49 -0
  32. edsl/prompts/Prompt.py +124 -61
  33. edsl/questions/descriptors.py +37 -23
  34. edsl/questions/question_base_gen_mixin.py +1 -0
  35. edsl/results/DatasetExportMixin.py +35 -6
  36. edsl/results/Result.py +9 -3
  37. edsl/results/Results.py +180 -2
  38. edsl/results/ResultsGGMixin.py +117 -60
  39. edsl/scenarios/PdfExtractor.py +3 -6
  40. edsl/scenarios/Scenario.py +35 -1
  41. edsl/scenarios/ScenarioList.py +22 -3
  42. edsl/scenarios/ScenarioListPdfMixin.py +9 -3
  43. edsl/surveys/Survey.py +1 -1
  44. edsl/templates/error_reporting/base.html +2 -4
  45. edsl/templates/error_reporting/exceptions_table.html +35 -0
  46. edsl/templates/error_reporting/interview_details.html +67 -53
  47. edsl/templates/error_reporting/interviews.html +4 -17
  48. edsl/templates/error_reporting/overview.html +31 -5
  49. edsl/templates/error_reporting/performance_plot.html +1 -1
  50. {edsl-0.1.41.dist-info → edsl-0.1.43.dist-info}/METADATA +2 -3
  51. {edsl-0.1.41.dist-info → edsl-0.1.43.dist-info}/RECORD +53 -51
  52. {edsl-0.1.41.dist-info → edsl-0.1.43.dist-info}/LICENSE +0 -0
  53. {edsl-0.1.41.dist-info → edsl-0.1.43.dist-info}/WHEEL +0 -0
@@ -358,7 +358,41 @@ class Scenario(Base, UserDict, ScenarioHtmlMixin):
358
358
  def from_pdf(cls, pdf_path: str):
359
359
  from edsl.scenarios.PdfExtractor import PdfExtractor
360
360
 
361
- return PdfExtractor(pdf_path, cls).get_object()
361
+ extractor = PdfExtractor(pdf_path)
362
+ return Scenario(extractor.get_pdf_dict())
363
+
364
+ @classmethod
365
+ def from_pdf_to_image(cls, pdf_path, image_format="jpeg"):
366
+ """
367
+ Convert each page of a PDF into an image and create key/value for it.
368
+
369
+ :param pdf_path: Path to the PDF file.
370
+ :param image_format: Format of the output images (default is 'jpeg').
371
+ :return: ScenarioList instance containing the Scenario instances.
372
+
373
+ The scenario has a key "filepath" and one or more keys "page_{i}" for each page.
374
+ """
375
+ import tempfile
376
+ from pdf2image import convert_from_path
377
+ from edsl.scenarios import Scenario
378
+
379
+ with tempfile.TemporaryDirectory() as output_folder:
380
+ # Convert PDF to images
381
+ images = convert_from_path(pdf_path)
382
+
383
+ scenario_dict = {"filepath":pdf_path}
384
+
385
+ # Save each page as an image and create Scenario instances
386
+ for i, image in enumerate(images):
387
+ image_path = os.path.join(output_folder, f"page_{i}.{image_format}")
388
+ image.save(image_path, image_format.upper())
389
+
390
+ from edsl import FileStore
391
+ scenario_dict[f"page_{i}"] = FileStore(image_path)
392
+
393
+ scenario = Scenario(scenario_dict)
394
+
395
+ return cls(scenario)
362
396
 
363
397
  @classmethod
364
398
  def from_docx(cls, docx_path: str) -> "Scenario":
@@ -1135,7 +1135,7 @@ class ScenarioList(Base, UserList, ScenarioListMixin):
1135
1135
  return cls(observations)
1136
1136
 
1137
1137
  @classmethod
1138
- def from_google_sheet(cls, url: str, sheet_name: str = None) -> ScenarioList:
1138
+ def from_google_sheet(cls, url: str, sheet_name: str = None, column_names: Optional[List[str]]= None) -> ScenarioList:
1139
1139
  """Create a ScenarioList from a Google Sheet.
1140
1140
 
1141
1141
  This method downloads the Google Sheet as an Excel file, saves it to a temporary file,
@@ -1145,6 +1145,8 @@ class ScenarioList(Base, UserList, ScenarioListMixin):
1145
1145
  url (str): The URL to the Google Sheet.
1146
1146
  sheet_name (str, optional): The name of the sheet to load. If None, the method will behave
1147
1147
  the same as from_excel regarding multiple sheets.
1148
+ column_names (List[str], optional): If provided, use these names for the columns instead
1149
+ of the default column names from the sheet.
1148
1150
 
1149
1151
  Returns:
1150
1152
  ScenarioList: An instance of the ScenarioList class.
@@ -1172,8 +1174,25 @@ class ScenarioList(Base, UserList, ScenarioListMixin):
1172
1174
  temp_file.write(response.content)
1173
1175
  temp_filename = temp_file.name
1174
1176
 
1175
- # Call the from_excel class method with the temporary file
1176
- return cls.from_excel(temp_filename, sheet_name=sheet_name)
1177
+ # First create the ScenarioList with default column names
1178
+ scenario_list = cls.from_excel(temp_filename, sheet_name=sheet_name)
1179
+
1180
+ # If column_names is provided, create a new ScenarioList with the specified names
1181
+ if column_names is not None:
1182
+ if len(column_names) != len(scenario_list[0].keys()):
1183
+ raise ValueError(
1184
+ f"Number of provided column names ({len(column_names)}) "
1185
+ f"does not match number of columns in sheet ({len(scenario_list[0].keys())})"
1186
+ )
1187
+
1188
+ # Create a codebook mapping original keys to new names
1189
+ original_keys = list(scenario_list[0].keys())
1190
+ codebook = dict(zip(original_keys, column_names))
1191
+
1192
+ # Return new ScenarioList with renamed columns
1193
+ return scenario_list.rename(codebook)
1194
+ else:
1195
+ return scenario_list
1177
1196
 
1178
1197
  @classmethod
1179
1198
  def from_delimited_file(
@@ -148,13 +148,15 @@ class ScenarioListPdfMixin:
148
148
  return False
149
149
 
150
150
  @classmethod
151
- def _from_pdf_to_image(cls, pdf_path, image_format="jpeg"):
151
+ def from_pdf_to_image(cls, pdf_path, image_format="jpeg"):
152
152
  """
153
153
  Convert each page of a PDF into an image and create Scenario instances.
154
154
 
155
155
  :param pdf_path: Path to the PDF file.
156
156
  :param image_format: Format of the output images (default is 'jpeg').
157
157
  :return: ScenarioList instance containing the Scenario instances.
158
+
159
+ The scenario list has keys "filepath", "page", "content".
158
160
  """
159
161
  import tempfile
160
162
  from pdf2image import convert_from_path
@@ -171,10 +173,14 @@ class ScenarioListPdfMixin:
171
173
  image_path = os.path.join(output_folder, f"page_{i+1}.{image_format}")
172
174
  image.save(image_path, image_format.upper())
173
175
 
174
- scenario = Scenario._from_filepath_image(image_path)
176
+ from edsl import FileStore
177
+ scenario = Scenario({
178
+ "filepath":image_path,
179
+ "page":i,
180
+ "content":FileStore(image_path)
181
+ })
175
182
  scenarios.append(scenario)
176
183
 
177
- # print(f"Saved {len(images)} pages as images in {output_folder}")
178
184
  return cls(scenarios)
179
185
 
180
186
  @staticmethod
edsl/surveys/Survey.py CHANGED
@@ -1281,4 +1281,4 @@ if __name__ == "__main__":
1281
1281
  import doctest
1282
1282
 
1283
1283
  # doctest.testmod(optionflags=doctest.ELLIPSIS | doctest.SKIP)
1284
- doctest.testmod(optionflags=doctest.ELLIPSIS)
1284
+ doctest.testmod(optionflags=doctest.ELLIPSIS)
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Exception Details</title>
6
+ <title>Exceptions Report</title>
7
7
  <style>
8
8
  {{ css }}
9
9
  </style>
@@ -15,9 +15,7 @@
15
15
  </head>
16
16
  <body>
17
17
  {% include 'overview.html' %}
18
- {% include 'exceptions_by_type.html' %}
19
- {% include 'exceptions_by_model.html' %}
20
- {% include 'exceptions_by_question_name.html' %}
18
+ {% include 'exceptions_table.html' %}
21
19
  {% include 'interviews.html' %}
22
20
  {% include 'performance_plot.html' %}
23
21
  </body>
@@ -0,0 +1,35 @@
1
+ <style>
2
+ th, td {
3
+ padding: 0 10px; /* This applies the padding uniformly to all td elements */
4
+ }
5
+ </style>
6
+
7
+ <table border="1">
8
+ <thead>
9
+ <tr>
10
+ <th>Exception Type</th>
11
+ <th>Service</th>
12
+ <th>Model</th>
13
+ <th>Question Name</th>
14
+ <th>Total</th>
15
+ </tr>
16
+ </thead>
17
+ <tbody>
18
+ {% for (exception_type, service, model, question_name), count in exceptions_table.items() %}
19
+ <tr>
20
+ <td>{{ exception_type }}</td>
21
+ <td>{{ service }}</td>
22
+ <td>{{ model }}</td>
23
+ <td>{{ question_name }}</td>
24
+ <td>{{ count }}</td>
25
+ </tr>
26
+ {% endfor %}
27
+ </tbody>
28
+ </table>
29
+ <p>
30
+ <i>Note:</i> You may encounter repeated exceptions where retries were attempted.
31
+ You can modify the maximum number of attempts for failed API calls in `edsl/config.py`.
32
+ </p>
33
+ <p>
34
+ Click to expand the details below for information about each exception, including code for reproducing it.
35
+ </p>
@@ -1,43 +1,67 @@
1
- <div class="question">question_name: {{ question }}</div>
2
-
1
+ <style>
2
+ td {
3
+ padding: 0 10px; /* This applies the padding uniformly to all td elements */
4
+ }
5
+ .toggle-btn {
6
+ background-color: #4CAF50;
7
+ color: white;
8
+ border: none;
9
+ padding: 10px 20px;
10
+ text-align: center;
11
+ text-decoration: none;
12
+ display: inline-block;
13
+ font-size: 16px;
14
+ margin: 4px 2px;
15
+ cursor: pointer;
16
+ border-radius: 8px;
17
+ white-space: nowrap;
18
+ }
19
+ .toggle-btn span.collapse {
20
+ display: none;
21
+ }
22
+ .exception-content {
23
+ max-width: 100%; /* Adjust this value based on your layout */
24
+ overflow-x: auto; /* Enables horizontal scrolling */
25
+ }
26
+ </style>
3
27
 
4
- <h2>Exception details</h2>
28
+ <div class="question">question_name: {{ question }}</div>
5
29
 
6
30
  {% for exception_message in exceptions %}
7
31
  <div class="exception-detail">
8
- <div class="exception-header">
32
+ <div class="exception-header">
9
33
  <span class="exception-exception">Exception: {{ exception_message.name }}</span>
10
- <button class="toggle-btn">▼</button>
11
- </div>
12
- <div class="exception-content">
34
+ <button id="toggleBtn" class="toggle-btn" onclick="toggleButton(this)" aria-expanded="false">
35
+ <span class="expand"> ▼ </span>
36
+ </button>
37
+ </div>
38
+ <div class="exception-content">
13
39
  <table border="1">
14
- <tr>
15
- <th>Key</th>
16
- <th>Value</th>
17
- </tr>
18
40
  <tr>
19
41
  <td>Interview ID (index in results)</td>
20
42
  <td>{{ index }}</td>
21
43
  </tr>
22
44
  <tr>
23
- <td>Question name (question_name)</td>
45
+ <td>Question name</td>
24
46
  <td>{{ question }}</td>
25
47
  </tr>
26
-
27
48
  <tr>
28
- <td>Question type (question_type)</td>
49
+ <td>Question type</td>
29
50
  <td>{{ exception_message.question_type }}</td>
30
51
  </tr>
31
-
32
52
  <tr>
33
53
  <td>Human-readable question</td>
34
54
  <td>{{ interview.survey._get_question_by_name(question).html(
35
55
  scenario = interview.scenario,
36
56
  agent = interview.agent,
37
- answers = exception_message.answers)
38
-
57
+ answers = exception_message.answers
58
+ )
39
59
  }}</td>
40
60
  </tr>
61
+ <tr>
62
+ <td>User Prompt</td>
63
+ <td><pre>{{ exception_message.rendered_prompts['user_prompt'] }}</pre></td>
64
+ </tr>
41
65
  <tr>
42
66
  <td>Scenario</td>
43
67
  <td>{{ interview.scenario.__repr__() }}</td>
@@ -47,24 +71,20 @@
47
71
  <td>{{ interview.agent.__repr__() }}</td>
48
72
  </tr>
49
73
  <tr>
50
- <td>Model name</td>
51
- <td>{{ interview.model.model }}</td>
74
+ <td>System Prompt</td>
75
+ <td><pre>{{ exception_message.rendered_prompts['system_prompt'] }}</pre></td>
52
76
  </tr>
53
77
  <tr>
54
78
  <td>Inference service</td>
55
79
  <td>{{ interview.model._inference_service_ }}</td>
56
80
  </tr>
57
81
  <tr>
58
- <td>Model parameters</td>
59
- <td>{{ interview.model.__repr__() }}</td>
60
- </tr>
61
- <tr>
62
- <td>User Prompt</td>
63
- <td><pre>{{ exception_message.rendered_prompts['user_prompt'] }}</pre></td>
82
+ <td>Model name</td>
83
+ <td>{{ interview.model.model }}</td>
64
84
  </tr>
65
85
  <tr>
66
- <td>System Prompt</td>
67
- <td><pre>{{ exception_message.rendered_prompts['system_prompt'] }}</pre></td>
86
+ <td>Model parameters</td>
87
+ <td>{{ interview.model.__repr__() }}</td>
68
88
  </tr>
69
89
  <tr>
70
90
  <td>Raw model response</td>
@@ -77,7 +97,7 @@
77
97
  </td>
78
98
  </tr>
79
99
  <tr>
80
- <td>Code to (likely) reproduce the error</td>
100
+ <td>Code likely to reproduce the error</td>
81
101
  <td>
82
102
  <textarea id="codeToCopy" rows="10" cols="90">{{ exception_message.code_to_reproduce }}</textarea>
83
103
  <button onclick="copyCode()">Copy</button>
@@ -85,32 +105,26 @@
85
105
  </tr>
86
106
 
87
107
  </table>
88
-
89
-
90
- {% if exception_message.exception.__class__.__name__ == 'QuestionAnswerValidationError' %}
91
- <h3>Answer validation details</h3>
92
- <table border="1">
93
- <tr>
94
- <th>Field</th>
95
- <th>Value</th>
96
- </tr>
97
- {% for field, (explanation, open_tag, close_tag, value) in exception_message.exception.to_html_dict().items() %}
98
-
99
- <tr>
100
- <td>{{ field }}: ({{ explanation }})</td>
101
- <td><{{open_tag}}> {{ value | escape }} <{{close_tag}}></td>
102
- </tr>
103
- {% endfor %}
104
- </table>
105
- {% endif %}
106
-
107
- <div class="exception-time">Time: {{ exception_message.time }}</div>
108
- <div class="exception-traceback">Traceback:
109
- <text>
110
- <pre>{{ exception_message.traceback }}</pre>
111
- </text>
112
- </div>
108
+
109
+ {% if exception_message.exception.__class__.__name__ == 'QuestionAnswerValidationError' %}
110
+ <h3>Answer validation details</h3>
111
+ <table border="1">
112
+ {% for field, (open_tag, close_tag, value) in exception_message.exception.to_html_dict().items() %}
113
+ <tr>
114
+ <td>{{ field }}</td>
115
+ <td><{{ open_tag }}> {{ value | escape }} <{{ close_tag }}></td>
116
+ </tr>
117
+ {% endfor %}
118
+ </table>
119
+ {% endif %}
120
+ <br><br>
121
+ <div class="exception-time">Time: {{ exception_message.time }}</div>
122
+ <div class="exception-traceback">Traceback:
123
+ <text>
124
+ <pre>{{ exception_message.traceback }}</pre>
125
+ </text>
113
126
  </div>
114
127
  </div>
128
+ </div>
115
129
 
116
130
  {% endfor %}
@@ -1,19 +1,6 @@
1
-
2
- {% if interviews|length > max_interviews %}
3
- <h1>Only showing the first {{ max_interviews }} interviews with errors</h1>
4
- {% else %}
5
- <h1>Showing all interviews</h1>
6
- {% endif %}
7
-
1
+ <h2>Exceptions Details</h2>
8
2
  {% for index, interview in interviews.items() %}
9
- {% if index < max_interviews %}
10
- {% if interview.exceptions != {} %}
11
- <div class="interview">Interview: {{ index }} </div>
12
- Model: {{ interview.model.model }}
13
- <h1>Failing questions</h1>
14
- {% endif %}
15
- {% for question, exceptions in interview.exceptions.items() %}
16
- {% include 'interview_details.html' %}
17
- {% endfor %}
18
- {% endif %}
3
+ {% for question, exceptions in interview.exceptions.items() %}
4
+ {% include 'interview_details.html' %}
5
+ {% endfor %}
19
6
  {% endfor %}
@@ -1,5 +1,31 @@
1
- <h1>Overview</h1>
2
- <p>There were {{ interviews|length }} total interview(s). An 'interview' is the result of one survey, taken by one agent, with one model, with one scenario.</p>
3
- The number of interviews with any exceptions was {{ num_exceptions }}.</p>
4
- <p>For advice on dealing with exceptions on Expected Parrot,
5
- see <a href="https://docs.expectedparrot.com/en/latest/exceptions.html">here</a>.</p>
1
+ <style>
2
+ td {
3
+ padding: 0 10px; /* This applies the padding uniformly to all td elements */
4
+ }
5
+ </style>
6
+
7
+ <h1>Exceptions Report</h1>
8
+ <p>
9
+ This report summarizes exceptions encountered in the job that was run.
10
+ </p>
11
+ <p>
12
+ For advice on dealing with exceptions, please see the EDSL <a href="https://docs.expectedparrot.com/en/latest/exceptions.html">documentation</a> page. <br>
13
+ You can also post a question at the Expected Parrot <a href="https://discord.com/invite/mxAYkjfy9m">Discord channel</a>, open an issue on <a href="https://github.com/expectedparrot/edsl">GitHub</a>, or send an email to <a href="mailto:info@expectedparrot.com">info@expectedparrot.com</a>.
14
+ </p>
15
+
16
+ <h2>Overview</h2>
17
+ <table border="1">
18
+ <tbody>
19
+ <tr>
20
+ <td>Total interviews</td>
21
+ <td>{{ interviews|length }}</td>
22
+ </tr>
23
+ <tr>
24
+ <td>Interviews with exceptions</td>
25
+ <td>{{ num_exceptions }}</td>
26
+ </tr>
27
+ </tbody>
28
+ </table>
29
+ <p>
30
+ An "interview" is the result of one survey, taken by one agent, with one model and one scenario (if any).
31
+ </p>
@@ -1,2 +1,2 @@
1
- <h1>Performance Plot</h1>
1
+ <h2>Performance Plot</h2>
2
2
  {{ performance_plot_html }}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: edsl
3
- Version: 0.1.41
3
+ Version: 0.1.43
4
4
  Summary: Create and analyze LLM-based surveys
5
5
  Home-page: https://www.expectedparrot.com/
6
6
  License: MIT
@@ -17,7 +17,7 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
17
  Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
18
18
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
19
  Requires-Dist: aiohttp (>=3.9.1,<4.0.0)
20
- Requires-Dist: anthropic (>=0.23.1,<0.24.0)
20
+ Requires-Dist: anthropic (>=0.45.0,<0.46.0)
21
21
  Requires-Dist: azure-ai-inference (>=1.0.0b3,<2.0.0)
22
22
  Requires-Dist: black[jupyter] (>=24.4.2,<25.0.0)
23
23
  Requires-Dist: boto3 (>=1.34.161,<2.0.0)
@@ -37,7 +37,6 @@ Requires-Dist: pandas (>=2.1.4,<3.0.0)
37
37
  Requires-Dist: platformdirs (>=4.3.6,<5.0.0)
38
38
  Requires-Dist: pydot (>=2.0.0,<3.0.0)
39
39
  Requires-Dist: pygments (>=2.17.2,<3.0.0)
40
- Requires-Dist: pymupdf (>=1.24.4,<2.0.0)
41
40
  Requires-Dist: pypdf2 (>=3.0.1,<4.0.0)
42
41
  Requires-Dist: pyreadstat (>=1.2.7,<2.0.0)
43
42
  Requires-Dist: python-docx (>=1.1.0,<2.0.0)