File size: 9,323 Bytes
27cdb3e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
"""
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"])