tuning error recognition
This commit is contained in:
@@ -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.
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user