Initial project setup: Obsidian intelligent journal organizer
- Add core agent architecture with Command + Skill pattern - Implement Claude API integration for content analysis - Add Obsidian REST API integration for vault operations - Create conversational interface (v2.0) with natural language processing - Add comprehensive configuration management and validation - Include project documentation and developer guides - Set up testing framework with unit, integration, and property tests - Add Kiro specs for Claude API configuration and code quality improvements - Configure project steering files for development guidelines
This commit is contained in:
@@ -0,0 +1,341 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user