""" 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"])