DevOps_Debugger / fingerprint /classifier.py
printf-sourav's picture
Initial commit
27cdb3e
Raw
History Blame Contribute Delete
9.32 kB
"""
Error Fingerprinting System — Classifies terminal errors into actionable categories.
Uses rule-based regex patterns to identify error types before the LLM agent
generates a fix command. This gives the agent better context and enables
analysis of which error categories the agent struggles with.
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from typing import Dict, List, Optional, Tuple
# Canonical error type taxonomy
ERROR_TYPES: List[str] = [
"missing_package", # ModuleNotFoundError, ImportError
"port_conflict", # Address already in use
"missing_env", # KeyError on env var, undefined variable
"permission_denied", # PermissionError
"version_conflict", # incompatible package versions
"syntax_error", # Python SyntaxError
"config_error", # misconfiguration in app config
"service_not_running", # failed to connect, connection refused
"file_not_found", # FileNotFoundError, No such file
"unknown", # unclassified
]
@dataclass
class FingerprintResult:
"""Result of error classification.
Attributes:
error_type: The classified error type from ERROR_TYPES.
confidence: Confidence score (0.0 to 1.0).
matched_pattern: The regex pattern that matched.
matched_text: The text fragment that matched.
suggested_category: Alternative error type if confidence is low.
"""
error_type: str
confidence: float
matched_pattern: str = ""
matched_text: str = ""
suggested_category: str = ""
# Regex patterns for each error type, ordered by specificity (most specific first)
_FINGERPRINT_RULES: List[Tuple[str, str, float]] = [
# (error_type, regex_pattern, base_confidence)
# Missing package — most common
("missing_package", r"ModuleNotFoundError:\s*No module named\s+['\"]?(\w+)", 0.95),
("missing_package", r"ImportError:\s*No module named\s+['\"]?(\w+)", 0.95),
("missing_package", r"ModuleNotFoundError", 0.85),
("missing_package", r"ImportError.*cannot import", 0.80),
("missing_package", r"No module named", 0.90),
# Port conflict
("port_conflict", r"Address already in use", 0.95),
("port_conflict", r"EADDRINUSE", 0.95),
("port_conflict", r"port\s+\d+\s+(is\s+)?(already\s+)?in\s+use", 0.90),
("port_conflict", r"bind\(\).*failed", 0.75),
("port_conflict", r"Errno\s+98", 0.90),
# Missing environment variable
("missing_env", r"KeyError:\s*['\"]([A-Z_]+)['\"]", 0.95),
("missing_env", r"undefined.*variable", 0.85),
("missing_env", r"environment variable.*not set", 0.90),
("missing_env", r"os\.environ\[", 0.80),
("missing_env", r"env.*not found", 0.75),
# Permission denied
("permission_denied", r"PermissionError", 0.95),
("permission_denied", r"Permission denied", 0.95),
("permission_denied", r"EACCES", 0.90),
("permission_denied", r"Operation not permitted", 0.90),
("permission_denied", r"Access denied", 0.85),
# Version conflict
("version_conflict", r"ResolutionImpossible", 0.95),
("version_conflict", r"version.*conflict", 0.90),
("version_conflict", r"incompatible.*version", 0.85),
("version_conflict", r"conflicting\s+dependencies", 0.90),
("version_conflict", r"requires.*but.*installed", 0.85),
("version_conflict", r"package versions have conflicting", 0.95),
# Syntax error
("syntax_error", r"SyntaxError:\s*invalid syntax", 0.95),
("syntax_error", r"SyntaxError", 0.90),
("syntax_error", r"IndentationError", 0.90),
("syntax_error", r"TabError", 0.90),
("syntax_error", r"unexpected EOF", 0.85),
# Config error
("config_error", r"config.*error", 0.80),
("config_error", r"misconfigur", 0.85),
("config_error", r"invalid.*config", 0.80),
("config_error", r"configuration.*failed", 0.80),
("config_error", r"binding.*error", 0.70),
# Service not running
("service_not_running", r"Connection refused", 0.90),
("service_not_running", r"ECONNREFUSED", 0.90),
("service_not_running", r"failed to connect", 0.85),
("service_not_running", r"service.*not.*running", 0.90),
("service_not_running", r"connection.*timed?\s*out", 0.75),
("service_not_running", r"could not connect", 0.85),
# File not found
("file_not_found", r"FileNotFoundError", 0.95),
("file_not_found", r"No such file or directory", 0.95),
("file_not_found", r"ENOENT", 0.90),
("file_not_found", r"(file|directory|command|script)\s+(not found|does not exist)", 0.65),
("file_not_found", r"bad interpreter", 0.80),
# NameError (often related to config/code errors)
("config_error", r"NameError:\s*name\s+['\"](\w+)['\"]", 0.80),
]
class ErrorFingerprinter:
"""Rule-based error classifier for terminal output.
Classifies error logs into one of the canonical ERROR_TYPES categories
using regex pattern matching. Provides confidence scores and matched
text for debugging.
Usage:
fp = ErrorFingerprinter()
result = fp.classify("ModuleNotFoundError: No module named 'flask'")
print(result.error_type) # "missing_package"
print(result.confidence) # 0.95
"""
def __init__(self, custom_rules: List[Tuple[str, str, float]] | None = None) -> None:
"""Initialize the fingerprinter.
Args:
custom_rules: Optional additional (error_type, regex, confidence) tuples.
"""
self.rules = list(_FINGERPRINT_RULES)
if custom_rules:
self.rules.extend(custom_rules)
def classify(self, error_log: str) -> FingerprintResult:
"""Classify an error log into an error type.
Scans the error log against all rules and returns the highest
confidence match.
Args:
error_log: The terminal error output to classify.
Returns:
FingerprintResult with the classified error type and metadata.
"""
if not error_log or not error_log.strip():
return FingerprintResult(
error_type="unknown",
confidence=0.0,
)
best_match: Optional[FingerprintResult] = None
second_best_type: str = ""
for error_type, pattern, base_confidence in self.rules:
match = re.search(pattern, error_log, re.IGNORECASE | re.MULTILINE)
if match:
# Boost confidence if we match multiple patterns for same type
confidence = base_confidence
if best_match is None or confidence > best_match.confidence:
if best_match:
second_best_type = best_match.error_type
best_match = FingerprintResult(
error_type=error_type,
confidence=confidence,
matched_pattern=pattern,
matched_text=match.group(0)[:200],
suggested_category=second_best_type,
)
elif confidence > 0.7 and error_type != best_match.error_type:
second_best_type = error_type
if best_match:
best_match.suggested_category = second_best_type
return best_match
return FingerprintResult(
error_type="unknown",
confidence=0.0,
)
def classify_with_all_matches(self, error_log: str) -> Dict[str, float]:
"""Return confidence scores for all error types found in the log.
Args:
error_log: The terminal error output to classify.
Returns:
Dict mapping error_type to highest confidence found.
"""
scores: Dict[str, float] = {}
for error_type, pattern, base_confidence in self.rules:
match = re.search(pattern, error_log, re.IGNORECASE | re.MULTILINE)
if match:
if error_type not in scores or base_confidence > scores[error_type]:
scores[error_type] = base_confidence
return scores
def get_error_summary(self, error_log: str) -> str:
"""Generate a one-line summary of the error for the agent prompt.
Args:
error_log: The terminal error output.
Returns:
Human-readable one-line error summary.
"""
result = self.classify(error_log)
summaries = {
"missing_package": "A required Python package is not installed.",
"port_conflict": "A network port is already in use by another process.",
"missing_env": "A required environment variable is not set.",
"permission_denied": "The operation lacks required permissions.",
"version_conflict": "Package versions are incompatible.",
"syntax_error": "The Python code has a syntax error.",
"config_error": "The application configuration is incorrect.",
"service_not_running": "A required service is not running or unreachable.",
"file_not_found": "A required file or directory does not exist.",
"unknown": "The error type could not be determined.",
}
return summaries.get(result.error_type, summaries["unknown"])