diff --git a/src/cli_explain_fix/knowledge_base.py b/src/cli_explain_fix/knowledge_base.py new file mode 100644 index 0000000..0e710e1 --- /dev/null +++ b/src/cli_explain_fix/knowledge_base.py @@ -0,0 +1,156 @@ +"""Knowledge base module for error explanations.""" + +import re +from dataclasses import dataclass +from typing import Dict, List, Optional, Any +from pathlib import Path + +import yaml + + +@dataclass +class ErrorExplanation: + """Container for error explanation data.""" + error_type: str + language: str + what_happened: str + why_happened: str + how_to_fix: List[str] + code_snippets: Optional[List[Dict[str, str]]] = None + documentation_url: Optional[str] = None + severity: str = "medium" + + def __post_init__(self): + if self.code_snippets is None: + self.code_snippets = [] + + +class KnowledgeBase: + """Knowledge base for error explanations.""" + + base_path: Path + + def __init__(self, base_path: Optional[str] = None): + """Initialize knowledge base with error definitions.""" + self._errors: Dict[str, Dict[str, Any]] = {} + self._patterns: Dict[str, List[Dict[str, Any]]] = {} + if base_path is None: + self.base_path = Path(__file__).parent.parent.parent / 'knowledge_base' + else: + self.base_path = Path(base_path) + self._load_knowledge_base() + + def _load_knowledge_base(self) -> None: + """Load all YAML files from knowledge base directory.""" + errors_file = self.base_path / 'errors.yaml' + patterns_file = self.base_path / 'patterns.yaml' + + if errors_file.exists(): + try: + with open(errors_file, 'r', encoding='utf-8') as f: + data = yaml.safe_load(f) or {} + if 'errors' in data: + for error_def in data['errors']: + key = f"{error_def.get('language', 'unknown')}_{error_def.get('error_type', 'Unknown')}" + self._errors[key] = error_def + except (yaml.YAMLError, OSError): + pass + + if patterns_file.exists(): + try: + with open(patterns_file, 'r', encoding='utf-8') as f: + data = yaml.safe_load(f) or {} + if 'patterns' in data: + self._patterns = data['patterns'] + except (yaml.YAMLError, OSError): + pass + + def get_explanation(self, error_type: str, language: str) -> Optional[ErrorExplanation]: + """Get explanation for a specific error type and language.""" + key = f"{language}_{error_type}" + if key in self._errors: + error_def = self._errors[key] + return ErrorExplanation( + error_type=error_type, + language=language, + what_happened=error_def.get('what_happened', 'An error occurred.'), + why_happened=error_def.get('why_happened', 'Unknown reason.'), + how_to_fix=error_def.get('how_to_fix', []), + code_snippets=error_def.get('code_snippets', []), + documentation_url=error_def.get('documentation_url'), + severity=error_def.get('severity', 'medium'), + ) + + generic_key = f"{language}_Generic" + if generic_key in self._errors: + error_def = self._errors[generic_key] + return ErrorExplanation( + error_type=error_type, + language=language, + what_happened=f"Received a {error_type} error.", + why_happened=error_def.get('why_happened', 'The operation failed due to an error.'), + how_to_fix=error_def.get('how_to_fix', []), + code_snippets=error_def.get('code_snippets', []), + documentation_url=error_def.get('documentation_url'), + severity=error_def.get('severity', 'medium'), + ) + + return None + + def find_by_pattern(self, message: str, language: str) -> Optional[ErrorExplanation]: + """Find explanation by matching error message against patterns.""" + if language not in self._patterns: + return None + + for pattern_def in self._patterns[language]: + pattern = pattern_def.get('pattern', '') + if re.search(pattern, message, re.IGNORECASE): + return ErrorExplanation( + error_type=pattern_def.get('error_type', 'Unknown'), + language=language, + what_happened=pattern_def.get('what_happened', ''), + why_happened=pattern_def.get('why_happened', ''), + how_to_fix=pattern_def.get('how_to_fix', []), + code_snippets=pattern_def.get('code_snippets', []), + documentation_url=pattern_def.get('documentation_url'), + severity=pattern_def.get('severity', 'medium'), + ) + + return None + + def list_languages(self) -> List[str]: + """List all supported languages.""" + languages = set() + for key in self._errors: + parts = key.split('_', 1) + if len(parts) >= 1: + languages.add(parts[0]) + return sorted(languages) + + def list_errors(self, language: Optional[str] = None) -> List[Dict[str, str]]: + """List all known errors, optionally filtered by language.""" + errors = [] + for key, error_def in self._errors.items(): + if language is None or key.startswith(f"{language}_"): + errors.append({ + 'error_type': error_def.get('error_type', key), + 'language': error_def.get('language', 'unknown'), + 'description': error_def.get('what_happened', '')[:100], + }) + return errors + + def get_fallback_explanation(self, error_type: str, language: str) -> ErrorExplanation: + """Generate a fallback explanation for unknown errors.""" + return ErrorExplanation( + error_type=error_type, + language=language, + what_happened=f"A {error_type} error was encountered.", + why_happened="The specific cause of this error is not in the knowledge base.", + how_to_fix=[ + "Check the error message for specific details about what went wrong.", + "Search for the exact error message online.", + f"Ensure you're using the correct syntax for {language}.", + "Verify that all required dependencies are installed.", + ], + severity="unknown", + )