edsl 0.1.54__py3-none-any.whl → 0.1.56__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 (105) hide show
  1. edsl/__init__.py +8 -1
  2. edsl/__init__original.py +134 -0
  3. edsl/__version__.py +1 -1
  4. edsl/agents/agent.py +29 -0
  5. edsl/agents/agent_list.py +36 -1
  6. edsl/base/base_class.py +281 -151
  7. edsl/base/data_transfer_models.py +15 -4
  8. edsl/buckets/__init__.py +8 -3
  9. edsl/buckets/bucket_collection.py +9 -3
  10. edsl/buckets/model_buckets.py +4 -2
  11. edsl/buckets/token_bucket.py +2 -2
  12. edsl/buckets/token_bucket_client.py +5 -3
  13. edsl/caching/cache.py +131 -62
  14. edsl/caching/cache_entry.py +70 -58
  15. edsl/caching/sql_dict.py +17 -0
  16. edsl/cli.py +99 -0
  17. edsl/config/config_class.py +16 -0
  18. edsl/conversation/__init__.py +31 -0
  19. edsl/coop/coop.py +276 -242
  20. edsl/coop/coop_jobs_objects.py +59 -0
  21. edsl/coop/coop_objects.py +29 -0
  22. edsl/coop/coop_regular_objects.py +26 -0
  23. edsl/coop/utils.py +24 -19
  24. edsl/dataset/dataset.py +338 -101
  25. edsl/dataset/dataset_operations_mixin.py +216 -180
  26. edsl/db_list/sqlite_list.py +349 -0
  27. edsl/inference_services/__init__.py +40 -5
  28. edsl/inference_services/exceptions.py +11 -0
  29. edsl/inference_services/services/anthropic_service.py +5 -2
  30. edsl/inference_services/services/aws_bedrock.py +6 -2
  31. edsl/inference_services/services/azure_ai.py +6 -2
  32. edsl/inference_services/services/google_service.py +7 -3
  33. edsl/inference_services/services/mistral_ai_service.py +6 -2
  34. edsl/inference_services/services/open_ai_service.py +6 -2
  35. edsl/inference_services/services/perplexity_service.py +6 -2
  36. edsl/inference_services/services/test_service.py +94 -5
  37. edsl/interviews/answering_function.py +167 -59
  38. edsl/interviews/interview.py +124 -72
  39. edsl/interviews/interview_task_manager.py +10 -0
  40. edsl/interviews/request_token_estimator.py +8 -0
  41. edsl/invigilators/invigilators.py +35 -13
  42. edsl/jobs/async_interview_runner.py +146 -104
  43. edsl/jobs/data_structures.py +6 -4
  44. edsl/jobs/decorators.py +61 -0
  45. edsl/jobs/fetch_invigilator.py +61 -18
  46. edsl/jobs/html_table_job_logger.py +14 -2
  47. edsl/jobs/jobs.py +180 -104
  48. edsl/jobs/jobs_component_constructor.py +2 -2
  49. edsl/jobs/jobs_interview_constructor.py +2 -0
  50. edsl/jobs/jobs_pricing_estimation.py +154 -113
  51. edsl/jobs/jobs_remote_inference_logger.py +4 -0
  52. edsl/jobs/jobs_runner_status.py +30 -25
  53. edsl/jobs/progress_bar_manager.py +79 -0
  54. edsl/jobs/remote_inference.py +35 -1
  55. edsl/key_management/key_lookup_builder.py +6 -1
  56. edsl/language_models/language_model.py +110 -12
  57. edsl/language_models/model.py +10 -3
  58. edsl/language_models/price_manager.py +176 -71
  59. edsl/language_models/registry.py +5 -0
  60. edsl/notebooks/notebook.py +77 -10
  61. edsl/questions/VALIDATION_README.md +134 -0
  62. edsl/questions/__init__.py +24 -1
  63. edsl/questions/exceptions.py +21 -0
  64. edsl/questions/question_dict.py +201 -16
  65. edsl/questions/question_multiple_choice_with_other.py +624 -0
  66. edsl/questions/question_registry.py +2 -1
  67. edsl/questions/templates/multiple_choice_with_other/__init__.py +0 -0
  68. edsl/questions/templates/multiple_choice_with_other/answering_instructions.jinja +15 -0
  69. edsl/questions/templates/multiple_choice_with_other/question_presentation.jinja +17 -0
  70. edsl/questions/validation_analysis.py +185 -0
  71. edsl/questions/validation_cli.py +131 -0
  72. edsl/questions/validation_html_report.py +404 -0
  73. edsl/questions/validation_logger.py +136 -0
  74. edsl/results/result.py +115 -46
  75. edsl/results/results.py +702 -171
  76. edsl/scenarios/construct_download_link.py +16 -3
  77. edsl/scenarios/directory_scanner.py +226 -226
  78. edsl/scenarios/file_methods.py +5 -0
  79. edsl/scenarios/file_store.py +150 -9
  80. edsl/scenarios/handlers/__init__.py +5 -1
  81. edsl/scenarios/handlers/mp4_file_store.py +104 -0
  82. edsl/scenarios/handlers/webm_file_store.py +104 -0
  83. edsl/scenarios/scenario.py +120 -101
  84. edsl/scenarios/scenario_list.py +800 -727
  85. edsl/scenarios/scenario_list_gc_test.py +146 -0
  86. edsl/scenarios/scenario_list_memory_test.py +214 -0
  87. edsl/scenarios/scenario_list_source_refactor.md +35 -0
  88. edsl/scenarios/scenario_selector.py +5 -4
  89. edsl/scenarios/scenario_source.py +1990 -0
  90. edsl/scenarios/tests/test_scenario_list_sources.py +52 -0
  91. edsl/surveys/survey.py +22 -0
  92. edsl/tasks/__init__.py +4 -2
  93. edsl/tasks/task_history.py +198 -36
  94. edsl/tests/scenarios/test_ScenarioSource.py +51 -0
  95. edsl/tests/scenarios/test_scenario_list_sources.py +51 -0
  96. edsl/utilities/__init__.py +2 -1
  97. edsl/utilities/decorators.py +121 -0
  98. edsl/utilities/memory_debugger.py +1010 -0
  99. {edsl-0.1.54.dist-info → edsl-0.1.56.dist-info}/METADATA +51 -76
  100. {edsl-0.1.54.dist-info → edsl-0.1.56.dist-info}/RECORD +103 -79
  101. edsl/jobs/jobs_runner_asyncio.py +0 -281
  102. edsl/language_models/unused/fake_openai_service.py +0 -60
  103. {edsl-0.1.54.dist-info → edsl-0.1.56.dist-info}/LICENSE +0 -0
  104. {edsl-0.1.54.dist-info → edsl-0.1.56.dist-info}/WHEEL +0 -0
  105. {edsl-0.1.54.dist-info → edsl-0.1.56.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,404 @@
1
+ """Generate an HTML report for validation failures.
2
+
3
+ This module provides functionality to create an HTML report of validation failures,
4
+ including statistics, suggestions for improvements, and examples of common failures.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+ from typing import Dict, List, Optional
12
+
13
+ from ..config import CONFIG
14
+ from .validation_analysis import (
15
+ get_validation_failure_stats,
16
+ suggest_fix_improvements,
17
+ export_improvements_report
18
+ )
19
+ from .validation_logger import get_validation_failure_logs
20
+
21
+ HTML_TEMPLATE = """
22
+ <!DOCTYPE html>
23
+ <html lang="en">
24
+ <head>
25
+ <meta charset="UTF-8">
26
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
27
+ <title>EDSL Validation Failures Report</title>
28
+ <style>
29
+ body {
30
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
31
+ line-height: 1.6;
32
+ color: #333;
33
+ max-width: 1200px;
34
+ margin: 0 auto;
35
+ padding: 20px;
36
+ }
37
+ h1, h2, h3, h4 {
38
+ color: #2c3e50;
39
+ }
40
+ .header {
41
+ border-bottom: 1px solid #eee;
42
+ padding-bottom: 10px;
43
+ margin-bottom: 20px;
44
+ display: flex;
45
+ justify-content: space-between;
46
+ align-items: center;
47
+ }
48
+ .timestamp {
49
+ color: #7f8c8d;
50
+ font-size: 0.9em;
51
+ }
52
+ .summary {
53
+ background-color: #f8f9fa;
54
+ border-radius: 5px;
55
+ padding: 15px;
56
+ margin-bottom: 20px;
57
+ }
58
+ .stats-container, .suggestions-container, .examples-container {
59
+ margin-bottom: 30px;
60
+ }
61
+ table {
62
+ width: 100%;
63
+ border-collapse: collapse;
64
+ margin-bottom: 20px;
65
+ }
66
+ th, td {
67
+ padding: 12px 15px;
68
+ text-align: left;
69
+ border-bottom: 1px solid #ddd;
70
+ }
71
+ th {
72
+ background-color: #f8f9fa;
73
+ font-weight: 600;
74
+ }
75
+ tr:hover {
76
+ background-color: #f5f5f5;
77
+ }
78
+ .suggestion {
79
+ background-color: #e3f2fd;
80
+ border-left: 4px solid #2196f3;
81
+ padding: 10px 15px;
82
+ margin-bottom: 10px;
83
+ border-radius: 0 4px 4px 0;
84
+ }
85
+ .card {
86
+ border: 1px solid #ddd;
87
+ border-radius: 4px;
88
+ padding: 15px;
89
+ margin-bottom: 20px;
90
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
91
+ }
92
+ .card-header {
93
+ font-weight: 600;
94
+ margin-bottom: 10px;
95
+ padding-bottom: 10px;
96
+ border-bottom: 1px solid #eee;
97
+ }
98
+ .example {
99
+ background-color: #fff8e1;
100
+ border-left: 4px solid #ffc107;
101
+ padding: 10px 15px;
102
+ margin-bottom: 10px;
103
+ border-radius: 0 4px 4px 0;
104
+ overflow-x: auto;
105
+ }
106
+ pre {
107
+ background-color: #f5f5f5;
108
+ padding: 10px;
109
+ border-radius: 4px;
110
+ overflow-x: auto;
111
+ }
112
+ code {
113
+ font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
114
+ font-size: 0.9em;
115
+ }
116
+ .badge {
117
+ display: inline-block;
118
+ padding: 3px 7px;
119
+ font-size: 0.75em;
120
+ font-weight: 600;
121
+ line-height: 1;
122
+ text-align: center;
123
+ white-space: nowrap;
124
+ vertical-align: baseline;
125
+ border-radius: 10px;
126
+ background-color: #e9ecef;
127
+ margin-right: 5px;
128
+ }
129
+ .badge-warning {
130
+ background-color: #fff3cd;
131
+ color: #856404;
132
+ }
133
+ .badge-primary {
134
+ background-color: #cfe2ff;
135
+ color: #084298;
136
+ }
137
+ .badge-success {
138
+ background-color: #d1e7dd;
139
+ color: #0f5132;
140
+ }
141
+ .fix-method {
142
+ background-color: #e8f5e9;
143
+ border-left: 4px solid #4caf50;
144
+ padding: 10px 15px;
145
+ margin: 10px 0;
146
+ border-radius: 0 4px 4px 0;
147
+ }
148
+ </style>
149
+ </head>
150
+ <body>
151
+ <div class="header">
152
+ <h1>EDSL Validation Failures Report</h1>
153
+ <span class="timestamp">Generated on {{timestamp}}</span>
154
+ </div>
155
+
156
+ <div class="summary">
157
+ <h2>Summary</h2>
158
+ <p>This report analyzes validation failures that occurred when question answers didn't meet the expected format or constraints.
159
+ It provides statistics, improvement suggestions for fix methods, and examples of common failures.</p>
160
+ <p><strong>Total validation failures:</strong> {{total_failures}}</p>
161
+ <p><strong>Question types with failures:</strong> {{question_types_count}}</p>
162
+ </div>
163
+
164
+ <div class="stats-container">
165
+ <h2>Validation Failure Statistics</h2>
166
+
167
+ <div class="card">
168
+ <div class="card-header">Failures by Question Type</div>
169
+ <table>
170
+ <thead>
171
+ <tr>
172
+ <th>Question Type</th>
173
+ <th>Failure Count</th>
174
+ <th>Percentage</th>
175
+ </tr>
176
+ </thead>
177
+ <tbody>
178
+ {{type_stats_rows}}
179
+ </tbody>
180
+ </table>
181
+ </div>
182
+
183
+ <div class="card">
184
+ <div class="card-header">Top Error Messages</div>
185
+ <table>
186
+ <thead>
187
+ <tr>
188
+ <th>Error Message</th>
189
+ <th>Occurrence Count</th>
190
+ </tr>
191
+ </thead>
192
+ <tbody>
193
+ {{error_stats_rows}}
194
+ </tbody>
195
+ </table>
196
+ </div>
197
+ </div>
198
+
199
+ <div class="suggestions-container">
200
+ <h2>Fix Method Improvement Suggestions</h2>
201
+ {{suggestions_content}}
202
+ </div>
203
+
204
+ <div class="examples-container">
205
+ <h2>Example Validation Failures</h2>
206
+ {{examples_content}}
207
+ </div>
208
+ </body>
209
+ </html>
210
+ """
211
+
212
+
213
+ def _generate_type_stats_rows(stats: Dict) -> str:
214
+ """Generate HTML table rows for question type statistics."""
215
+ type_stats = stats.get("by_question_type", {})
216
+ total_failures = sum(type_stats.values())
217
+
218
+ rows = []
219
+ for question_type, count in sorted(type_stats.items(), key=lambda x: x[1], reverse=True):
220
+ percentage = (count / total_failures) * 100 if total_failures > 0 else 0
221
+ row = (
222
+ f"<tr>"
223
+ f"<td>{question_type}</td>"
224
+ f"<td>{count}</td>"
225
+ f"<td>{percentage:.1f}%</td>"
226
+ f"</tr>"
227
+ )
228
+ rows.append(row)
229
+
230
+ return "\n".join(rows)
231
+
232
+
233
+ def _generate_error_stats_rows(stats: Dict) -> str:
234
+ """Generate HTML table rows for error message statistics."""
235
+ error_counts = {}
236
+
237
+ # Aggregate error counts across all question types
238
+ for question_type, errors in stats.get("by_error_message", {}).items():
239
+ for error_msg, count in errors.items():
240
+ error_counts[error_msg] = error_counts.get(error_msg, 0) + count
241
+
242
+ # Sort by count (descending)
243
+ sorted_errors = sorted(error_counts.items(), key=lambda x: x[1], reverse=True)
244
+
245
+ rows = []
246
+ for error_msg, count in sorted_errors[:10]: # Show top 10 errors
247
+ shortened_msg = error_msg[:100] + "..." if len(error_msg) > 100 else error_msg
248
+ row = (
249
+ f"<tr>"
250
+ f"<td>{shortened_msg}</td>"
251
+ f"<td>{count}</td>"
252
+ f"</tr>"
253
+ )
254
+ rows.append(row)
255
+
256
+ return "\n".join(rows)
257
+
258
+
259
+ def _generate_suggestions_content(suggestions: Dict) -> str:
260
+ """Generate HTML content for fix method suggestions."""
261
+ if not suggestions:
262
+ return "<p>No suggestions available. Log more validation failures to generate improvement suggestions.</p>"
263
+
264
+ content = []
265
+
266
+ for question_type, question_suggestions in suggestions.items():
267
+ content.append(f"<div class='card'>")
268
+ content.append(f"<div class='card-header'>{question_type}</div>")
269
+
270
+ for suggestion in question_suggestions:
271
+ error_msg = suggestion.get("error_message", "")
272
+ occurrence_count = suggestion.get("occurrence_count", 0)
273
+ suggestion_text = suggestion.get("suggestion", "")
274
+
275
+ content.append(
276
+ f"<div class='suggestion'>"
277
+ f"<p><strong>Error:</strong> {error_msg}</p>"
278
+ f"<p><strong>Occurrences:</strong> {occurrence_count}</p>"
279
+ f"<div class='fix-method'>"
280
+ f"<p><strong>Suggested improvement:</strong></p>"
281
+ f"<p>{suggestion_text}</p>"
282
+ f"</div>"
283
+ f"</div>"
284
+ )
285
+
286
+ content.append("</div>")
287
+
288
+ return "\n".join(content)
289
+
290
+
291
+ def _generate_examples_content(logs: List[Dict]) -> str:
292
+ """Generate HTML content for example validation failures."""
293
+ if not logs:
294
+ return "<p>No validation failure examples available.</p>"
295
+
296
+ content = []
297
+
298
+ # Group logs by question type
299
+ logs_by_type = {}
300
+ for log in logs:
301
+ question_type = log.get("question_type", "unknown")
302
+ if question_type not in logs_by_type:
303
+ logs_by_type[question_type] = []
304
+ logs_by_type[question_type].append(log)
305
+
306
+ # For each question type, show the most recent example
307
+ for question_type, type_logs in logs_by_type.items():
308
+ # Sort by timestamp (newest first)
309
+ sorted_logs = sorted(type_logs, key=lambda x: x.get("timestamp", ""), reverse=True)
310
+ example_log = sorted_logs[0]
311
+
312
+ error_message = example_log.get("error_message", "")
313
+ invalid_data = example_log.get("invalid_data", {})
314
+ model_schema = example_log.get("model_schema", {})
315
+
316
+ content.append(f"<div class='card'>")
317
+ content.append(f"<div class='card-header'>{question_type}</div>")
318
+
319
+ content.append(
320
+ f"<div class='example'>"
321
+ f"<p><strong>Error:</strong> {error_message}</p>"
322
+ f"<p><strong>Invalid Data:</strong></p>"
323
+ f"<pre><code>{json.dumps(invalid_data, indent=2)}</code></pre>"
324
+ f"<p><strong>Expected Schema:</strong></p>"
325
+ f"<pre><code>{json.dumps(model_schema, indent=2)}</code></pre>"
326
+ f"</div>"
327
+ )
328
+
329
+ content.append("</div>")
330
+
331
+ return "\n".join(content)
332
+
333
+
334
+ def generate_html_report(output_path: Optional[Path] = None) -> Path:
335
+ """
336
+ Generate an HTML report of validation failures.
337
+
338
+ Args:
339
+ output_path: Optional custom path for the report
340
+
341
+ Returns:
342
+ Path to the generated HTML report
343
+ """
344
+ # Determine output path
345
+ if output_path is None:
346
+ default_log_dir = Path.home() / ".edsl" / "logs"
347
+ try:
348
+ report_dir = Path(CONFIG.get("EDSL_LOG_DIR"))
349
+ except Exception:
350
+ # If EDSL_LOG_DIR is not defined, use default
351
+ report_dir = default_log_dir
352
+ os.makedirs(report_dir, exist_ok=True)
353
+ output_path = report_dir / "validation_report.html"
354
+
355
+ # Get validation data
356
+ logs = get_validation_failure_logs(n=100) # Get up to 100 recent logs
357
+ stats = get_validation_failure_stats()
358
+ suggestions = suggest_fix_improvements()
359
+
360
+ # Calculate summary statistics
361
+ total_failures = sum(stats.get("by_question_type", {}).values())
362
+ question_types_count = len(stats.get("by_question_type", {}))
363
+
364
+ # Generate report content
365
+ type_stats_rows = _generate_type_stats_rows(stats)
366
+ error_stats_rows = _generate_error_stats_rows(stats)
367
+ suggestions_content = _generate_suggestions_content(suggestions)
368
+ examples_content = _generate_examples_content(logs)
369
+
370
+ # Format timestamp
371
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
372
+
373
+ # Fill the template
374
+ html_content = HTML_TEMPLATE.replace("{{timestamp}}", timestamp)
375
+ html_content = html_content.replace("{{total_failures}}", str(total_failures))
376
+ html_content = html_content.replace("{{question_types_count}}", str(question_types_count))
377
+ html_content = html_content.replace("{{type_stats_rows}}", type_stats_rows)
378
+ html_content = html_content.replace("{{error_stats_rows}}", error_stats_rows)
379
+ html_content = html_content.replace("{{suggestions_content}}", suggestions_content)
380
+ html_content = html_content.replace("{{examples_content}}", examples_content)
381
+
382
+ # Write the report
383
+ with open(output_path, "w") as f:
384
+ f.write(html_content)
385
+
386
+ return output_path
387
+
388
+
389
+ def generate_and_open_report() -> None:
390
+ """Generate a validation report and open it in the default browser."""
391
+ report_path = generate_html_report()
392
+ print(f"Report generated at: {report_path}")
393
+
394
+ # Try to open the report in a browser
395
+ try:
396
+ import webbrowser
397
+ webbrowser.open(f"file://{report_path}")
398
+ except Exception as e:
399
+ print(f"Could not open browser: {e}")
400
+ print(f"Report is available at: {report_path}")
401
+
402
+
403
+ if __name__ == "__main__":
404
+ generate_and_open_report()
@@ -0,0 +1,136 @@
1
+ """Logger for validation failures in questions.
2
+
3
+ This module provides functionality to log validation failures that occur when
4
+ question answers don't meet the expected format or constraints. The logs can be
5
+ used to improve the "fix" methods for questions.
6
+ """
7
+
8
+ import datetime
9
+ import json
10
+ import logging
11
+ import os
12
+ import traceback
13
+ from pathlib import Path
14
+ from typing import Any, Dict, Optional
15
+
16
+ from ..config import CONFIG
17
+
18
+ # Set up logging
19
+ logger = logging.getLogger("validation_failures")
20
+ logger.setLevel(logging.INFO)
21
+
22
+ # Determine log directory path
23
+ DEFAULT_LOG_DIR = Path.home() / ".edsl" / "logs"
24
+ try:
25
+ LOG_DIR = Path(CONFIG.get("EDSL_LOG_DIR"))
26
+ except Exception:
27
+ # If EDSL_LOG_DIR is not defined, use default
28
+ LOG_DIR = DEFAULT_LOG_DIR
29
+ VALIDATION_LOG_FILE = LOG_DIR / "validation_failures.log"
30
+
31
+ # Create log directory if it doesn't exist
32
+ os.makedirs(LOG_DIR, exist_ok=True)
33
+
34
+ # Create file handler
35
+ file_handler = logging.FileHandler(VALIDATION_LOG_FILE)
36
+ file_handler.setLevel(logging.INFO)
37
+
38
+ # Create formatter
39
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
40
+ file_handler.setFormatter(formatter)
41
+
42
+ # Add handler to logger
43
+ logger.addHandler(file_handler)
44
+
45
+ # Touch the log file to make sure it exists
46
+ if not os.path.exists(VALIDATION_LOG_FILE):
47
+ with open(VALIDATION_LOG_FILE, 'a'):
48
+ pass
49
+
50
+
51
+ def log_validation_failure(
52
+ question_type: str,
53
+ question_name: str,
54
+ error_message: str,
55
+ invalid_data: Dict[str, Any],
56
+ model_schema: Dict[str, Any],
57
+ question_dict: Optional[Dict[str, Any]] = None,
58
+ ) -> None:
59
+ """
60
+ Log a validation failure to the validation failures log file.
61
+
62
+ Args:
63
+ question_type: The type of question that had a validation failure
64
+ question_name: The name of the question
65
+ error_message: The validation error message
66
+ invalid_data: The data that failed validation
67
+ model_schema: The schema of the model used for validation
68
+ question_dict: Optional dictionary representation of the question
69
+ """
70
+ log_entry = {
71
+ "timestamp": datetime.datetime.now().isoformat(),
72
+ "question_type": question_type,
73
+ "question_name": question_name,
74
+ "error_message": error_message,
75
+ "invalid_data": invalid_data,
76
+ "model_schema": model_schema,
77
+ "question_dict": question_dict,
78
+ "traceback": traceback.format_exc(),
79
+ }
80
+
81
+ # Log as JSON for easier parsing
82
+ logger.info(json.dumps(log_entry))
83
+
84
+ # Write directly to the file as well to ensure it's written
85
+ with open(VALIDATION_LOG_FILE, "a") as f:
86
+ f.write(f"{datetime.datetime.now().isoformat()} - validation_failures - INFO - {json.dumps(log_entry)}\n")
87
+ f.flush()
88
+
89
+
90
+ def get_validation_failure_logs(n: int = 10) -> list:
91
+ """
92
+ Get the latest n validation failure logs.
93
+
94
+ Args:
95
+ n: Number of logs to return (default: 10)
96
+
97
+ Returns:
98
+ List of validation failure log entries as dictionaries
99
+ """
100
+ logs = []
101
+
102
+ # Check if log file exists
103
+ if not os.path.exists(VALIDATION_LOG_FILE):
104
+ return logs
105
+
106
+ with open(VALIDATION_LOG_FILE, "r") as f:
107
+ for line in f:
108
+ try:
109
+ # Skip non-JSON lines (like logger initialization)
110
+ if not line.strip():
111
+ continue
112
+
113
+ # Handle both the Python logging format and our direct write format
114
+ parts = line.strip().split(" - ")
115
+ if len(parts) >= 4:
116
+ # Regular log line format: timestamp - name - level - message
117
+ json_part = parts[-1]
118
+ try:
119
+ log_entry = json.loads(json_part)
120
+ logs.append(log_entry)
121
+ except json.JSONDecodeError:
122
+ # Skip malformed JSON
123
+ continue
124
+ except (IndexError, ValueError):
125
+ # Skip malformed lines
126
+ continue
127
+
128
+ # Return most recent logs first
129
+ return sorted(logs, key=lambda x: x.get("timestamp", ""), reverse=True)[:n]
130
+
131
+
132
+ def clear_validation_logs() -> None:
133
+ """Clear all validation failure logs."""
134
+ if os.path.exists(VALIDATION_LOG_FILE):
135
+ with open(VALIDATION_LOG_FILE, "w") as f:
136
+ f.write("")