tuning error recognition

This commit is contained in:
Bryan Van Deusen
2026-01-30 22:04:07 -05:00
parent 097ab16c50
commit 3ee7de7ecd
7 changed files with 546 additions and 31 deletions
+162 -2
View File
@@ -1,9 +1,11 @@
"""Download history API endpoints."""
from quart import Blueprint, request, jsonify, current_app
from sqlalchemy import select, func, and_
from datetime import datetime, timedelta
from quart import Blueprint, request, jsonify, current_app, Response
from sqlalchemy import select, func, and_, or_, desc
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
import json
from app.models.base import utcnow
from app.models.download import Download, DownloadStatus
@@ -285,3 +287,161 @@ async def get_stats():
"queued": status_counts.get(DownloadStatus.QUEUED, 0),
"running": status_counts.get(DownloadStatus.RUNNING, 0),
})
@bp.route("/export-failed-logs", methods=["GET"])
async def export_failed_logs():
"""Export failed download logs for error classification analysis.
Returns a JSON file that can be fed to Claude to improve error recognition patterns.
Query params:
limit: Maximum number of records (default 50, max 200)
days: Only include failures from last N days (default 30)
error_type: Filter by specific error type (e.g., not_found, auth_error)
include_success: Also include some successful "no new content" for comparison
"""
limit = min(int(request.args.get("limit", 50)), 200)
days = int(request.args.get("days", 30))
error_type = request.args.get("error_type")
include_success = request.args.get("include_success", "").lower() in ("true", "1", "yes")
cutoff_date = utcnow() - timedelta(days=days)
async with current_app.db_engine.connect() as conn:
session = AsyncSession(bind=conn)
# Build query for failed downloads (eager load source for platform)
query = select(Download).options(selectinload(Download.source)).where(
and_(
Download.status == DownloadStatus.FAILED,
Download.created_at >= cutoff_date,
)
)
if error_type:
query = query.where(Download.error_type == error_type)
# Order by most recent first
query = query.order_by(desc(Download.created_at)).limit(limit)
result = await session.execute(query)
failed_downloads = result.scalars().all()
# Optionally include some successful NO_NEW_CONTENT for comparison
success_downloads = []
if include_success:
success_query = select(Download).options(selectinload(Download.source)).where(
and_(
Download.status == DownloadStatus.COMPLETED,
Download.created_at >= cutoff_date,
Download.file_count == 0, # No new content cases
)
).order_by(desc(Download.created_at)).limit(limit // 4)
success_result = await session.execute(success_query)
success_downloads = success_result.scalars().all()
# Format for export
exports = []
for download in failed_downloads:
exports.append(_format_download_for_export(download, "failed"))
for download in success_downloads:
exports.append(_format_download_for_export(download, "success_no_new_content"))
# Build output document
output = {
"export_info": {
"exported_at": utcnow().isoformat(),
"total_records": len(exports),
"failed_count": len(failed_downloads),
"success_comparison_count": len(success_downloads),
"days_included": days,
"error_type_filter": error_type,
},
"analysis_prompt": _generate_analysis_prompt(),
"downloads": exports,
}
# Return as downloadable JSON file
response = Response(
json.dumps(output, indent=2, default=str),
mimetype="application/json",
headers={
"Content-Disposition": f"attachment; filename=failed_logs_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
}
)
return response
def _format_download_for_export(download: Download, classification: str) -> dict:
"""Format a download record for export."""
# Extract logs from metadata
metadata = download.metadata_ or {}
stdout = metadata.get("stdout") or ""
stderr = metadata.get("stderr") or ""
# Truncate very long logs but keep enough for analysis
max_log_length = 50000 # 50KB per log field
if stdout and len(stdout) > max_log_length:
stdout = stdout[:max_log_length] + f"\n\n[... truncated, total length: {len(stdout)} chars ...]"
if stderr and len(stderr) > max_log_length:
stderr = stderr[:max_log_length] + f"\n\n[... truncated, total length: {len(stderr)} chars ...]"
# Get platform from source if available
platform = None
if download.source:
platform = download.source.platform
return {
"id": download.id,
"classification": classification,
"assigned_error_type": download.error_type,
"assigned_error_message": download.error_message,
"platform": platform,
"url": download.url,
"status": download.status, # Already a string, not enum
"file_count": download.file_count,
"created_at": download.created_at.isoformat() if download.created_at else None,
"duration_seconds": metadata.get("duration_seconds"),
"logs": {
"stdout": stdout,
"stderr": stderr,
},
"user_feedback": None, # User can fill this in: "correct", "false_positive", "false_negative", "wrong_type"
"suggested_error_type": None, # User can suggest what it should be
"notes": None, # User can add context
}
def _generate_analysis_prompt() -> str:
"""Generate a prompt for Claude to analyze the logs."""
return """
## Error Log Analysis Task
I'm providing you with download logs from a gallery-dl based downloader. Each record includes:
- The error_type that was assigned by the current classification logic
- The actual stdout/stderr logs from gallery-dl
- Platform and URL information
Please analyze these logs and identify:
1. **False Positives**: Cases where an error was reported but the download actually succeeded
- Look for: successful HTTP responses (200), "skipping" messages indicating archive deduplication worked
2. **False Negatives**: Cases classified as one error type that should be another
- Example: classified as "not_found" but logs show authentication issues
3. **Pattern Improvements**: Suggest more specific patterns to detect each error type
- Current patterns may be too broad (matching debug output) or too narrow (missing variations)
4. **New Error Types**: Any failure modes not currently categorized
For each issue found, provide:
- The download ID
- Current classification vs suggested classification
- The specific log lines that support your analysis
- Suggested pattern improvements (if applicable)
Focus on actionable improvements to the _categorize_error() function in gallery_dl.py.
"""