""" Dependency Management Module Handles optional dependencies with graceful degradation and helpful error messages """ import logging import sys from dataclasses import dataclass from pathlib import Path from typing import Dict, Any, Optional, List, Tuple, Callable @dataclass class DependencyInfo: """Information about a dependency""" name: str import_name: str install_command: str description: str required_for: List[str] minimum_version: Optional[str] = None alternative_packages: Optional[List[str]] = None setup_instructions: Optional[str] = None class DependencyManager: """Manages optional dependencies with graceful degradation""" def __init__(self): self.logger = logging.getLogger("DependencyManager") self._dependency_cache: Dict[str, Any] = {} self._availability_cache: Dict[str, bool] = {} # Define known dependencies self.dependencies = { 'anthropic': DependencyInfo( name='anthropic', import_name='anthropic', install_command='pip install anthropic>=0.25.0', description='Anthropic Claude API client for AI-powered content analysis', required_for=['Claude AI analysis', 'Natural language understanding', 'Content transformation'], minimum_version='0.25.0', setup_instructions=""" Claude API Setup: 1. Install: pip install anthropic>=0.25.0 2. Get API key from: https://console.anthropic.com/ 3. Set environment variable: export ANTHROPIC_API_KEY="sk-ant-your-key-here" 4. Or add to config.yaml: claude.api_key: "${ANTHROPIC_API_KEY}" """ ), 'aiohttp': DependencyInfo( name='aiohttp', import_name='aiohttp', install_command='pip install aiohttp>=3.9.0', description='Async HTTP client for Obsidian REST API integration', required_for=['Obsidian API communication', 'Reading/writing notes', 'File operations'], minimum_version='3.9.0', setup_instructions=""" HTTP Client Setup: 1. Install: pip install aiohttp>=3.9.0 2. Used for communicating with Obsidian Local REST API 3. No additional configuration required """ ), 'yaml': DependencyInfo( name='PyYAML', import_name='yaml', install_command='pip install pyyaml>=6.0', description='YAML parser for configuration files', required_for=['Configuration file parsing', 'Settings management'], minimum_version='6.0', alternative_packages=['ruamel.yaml'], setup_instructions=""" YAML Parser Setup: 1. Install: pip install pyyaml>=6.0 2. Alternative: pip install ruamel.yaml 3. Used for parsing config.yaml files """ ), 'pydantic': DependencyInfo( name='pydantic', import_name='pydantic', install_command='pip install pydantic>=2.0.0', description='Data validation and settings management', required_for=['Configuration validation', 'Data model validation', 'Type checking'], minimum_version='2.0.0', setup_instructions=""" Data Validation Setup: 1. Install: pip install pydantic>=2.0.0 2. Used for validating configuration files and data models 3. Provides enhanced error messages and type checking """ ) } def is_available(self, dependency_name: str) -> bool: """Check if a dependency is available""" if dependency_name in self._availability_cache: return self._availability_cache[dependency_name] if dependency_name not in self.dependencies: self.logger.warning(f"Unknown dependency: {dependency_name}") return False dep_info = self.dependencies[dependency_name] try: # Try to import the module __import__(dep_info.import_name) self._availability_cache[dependency_name] = True return True except ImportError: self._availability_cache[dependency_name] = False return False def get_module(self, dependency_name: str, raise_on_missing: bool = False) -> Optional[Any]: """Get a module if available, with optional error raising""" if dependency_name in self._dependency_cache: return self._dependency_cache[dependency_name] if not self.is_available(dependency_name): if raise_on_missing: raise ImportError(self._get_missing_dependency_message(dependency_name)) return None dep_info = self.dependencies[dependency_name] try: module = __import__(dep_info.import_name) self._dependency_cache[dependency_name] = module return module except ImportError as e: if raise_on_missing: raise ImportError(self._get_missing_dependency_message(dependency_name)) from e return None def get_class_from_module(self, dependency_name: str, class_name: str, raise_on_missing: bool = False) -> Optional[Any]: """Get a specific class from a module""" module = self.get_module(dependency_name, raise_on_missing=False) if module is None: if raise_on_missing: raise ImportError(self._get_missing_dependency_message(dependency_name)) return None try: return getattr(module, class_name) except AttributeError as e: if raise_on_missing: raise ImportError(f"Class {class_name} not found in {dependency_name}") from e return None def require_dependency(self, dependency_name: str) -> Any: """Require a dependency, raising detailed error if not available""" module = self.get_module(dependency_name, raise_on_missing=True) return module def check_all_dependencies(self) -> Dict[str, Dict[str, Any]]: """Check status of all known dependencies""" results = {} for dep_name, dep_info in self.dependencies.items(): is_avail = self.is_available(dep_name) results[dep_name] = { 'available': is_avail, 'name': dep_info.name, 'description': dep_info.description, 'required_for': dep_info.required_for, 'install_command': dep_info.install_command, 'setup_instructions': dep_info.setup_instructions } if is_avail: # Try to get version info try: module = self.get_module(dep_name) if hasattr(module, '__version__'): results[dep_name]['version'] = module.__version__ elif hasattr(module, 'version'): results[dep_name]['version'] = module.version except: pass return results def get_missing_dependencies(self) -> List[str]: """Get list of missing dependencies""" missing = [] for dep_name in self.dependencies: if not self.is_available(dep_name): missing.append(dep_name) return missing def get_installation_instructions(self, missing_only: bool = True) -> str: """Get installation instructions for dependencies""" deps_to_show = self.get_missing_dependencies() if missing_only else list(self.dependencies.keys()) if not deps_to_show: return "āœ“ All dependencies are available!" instructions = [] instructions.append("Missing Dependencies Installation Guide:") instructions.append("=" * 50) for dep_name in deps_to_show: dep_info = self.dependencies[dep_name] instructions.append(f"\nšŸ“¦ {dep_info.name}") instructions.append(f" Description: {dep_info.description}") instructions.append(f" Required for: {', '.join(dep_info.required_for)}") instructions.append(f" Install: {dep_info.install_command}") if dep_info.alternative_packages: instructions.append(f" Alternatives: {', '.join(dep_info.alternative_packages)}") if dep_info.setup_instructions: instructions.append(f" Setup:{dep_info.setup_instructions}") instructions.append("\n" + "=" * 50) instructions.append("Quick install all missing dependencies:") install_commands = [self.dependencies[dep].install_command for dep in deps_to_show] instructions.append(" && ".join(install_commands)) return "\n".join(instructions) def _get_missing_dependency_message(self, dependency_name: str) -> str: """Get detailed error message for missing dependency""" if dependency_name not in self.dependencies: return f"Unknown dependency: {dependency_name}" dep_info = self.dependencies[dependency_name] message_parts = [ f"Missing required dependency: {dep_info.name}", f"Description: {dep_info.description}", f"Required for: {', '.join(dep_info.required_for)}", "", f"To install: {dep_info.install_command}", ] if dep_info.alternative_packages: message_parts.append(f"Alternatives: {', '.join(dep_info.alternative_packages)}") if dep_info.setup_instructions: message_parts.append("") message_parts.append("Setup Instructions:") message_parts.append(dep_info.setup_instructions) return "\n".join(message_parts) def create_graceful_import_wrapper(self, dependency_name: str, fallback_message: Optional[str] = None): """Create a wrapper that provides graceful degradation for missing dependencies""" def wrapper(func: Callable) -> Callable: def inner(*args, **kwargs): if not self.is_available(dependency_name): error_msg = fallback_message or f"Feature unavailable: {dependency_name} is not installed" detailed_msg = self._get_missing_dependency_message(dependency_name) # Log the detailed message self.logger.error(f"Dependency missing: {detailed_msg}") # Return a user-friendly error try: from agent_core import SkillResult except ImportError: # Fallback if agent_core is not available class SkillResult: def __init__(self, success, error=None, message=""): self.success = success self.error = error self.message = message return SkillResult( success=False, error=error_msg, message=f"Please install {dependency_name} to use this feature" ) return func(*args, **kwargs) return inner return wrapper def get_dependency_status_report(self) -> str: """Get a comprehensive dependency status report""" all_deps = self.check_all_dependencies() available = [name for name, info in all_deps.items() if info['available']] missing = [name for name, info in all_deps.items() if not info['available']] report = [] report.append("Dependency Status Report") report.append("=" * 30) if available: report.append(f"\nāœ“ Available ({len(available)}):") for dep_name in available: info = all_deps[dep_name] version = info.get('version', 'unknown version') report.append(f" • {info['name']} ({version})") if missing: report.append(f"\nāœ— Missing ({len(missing)}):") for dep_name in missing: info = all_deps[dep_name] report.append(f" • {info['name']} - {info['description']}") report.append(f" Install: {info['install_command']}") if missing: report.append(f"\nTo install all missing dependencies:") install_commands = [all_deps[dep]['install_command'] for dep in missing] report.append(" && ".join(install_commands)) else: report.append(f"\nšŸŽ‰ All dependencies are available!") return "\n".join(report) # Global dependency manager instance dependency_manager = DependencyManager() def get_dependency_manager() -> DependencyManager: """Get the global dependency manager instance""" return dependency_manager def check_dependency(dependency_name: str) -> bool: """Quick check if a dependency is available""" return dependency_manager.is_available(dependency_name) def require_dependency(dependency_name: str) -> Any: """Require a dependency, raising detailed error if not available""" return dependency_manager.require_dependency(dependency_name) def get_optional_module(dependency_name: str) -> Optional[Any]: """Get a module if available, None otherwise""" return dependency_manager.get_module(dependency_name, raise_on_missing=False) def graceful_import(dependency_name: str, fallback_message: Optional[str] = None): """Decorator for graceful dependency handling""" return dependency_manager.create_graceful_import_wrapper(dependency_name, fallback_message)