""" Configuration migration utility for backward compatibility Handles migration from legacy configuration formats to new enhanced format """ import logging from typing import Dict, Any, List, Optional from pathlib import Path class ConfigurationMigrator: """Handle migration from legacy configuration formats""" def __init__(self): self.logger = logging.getLogger(__name__) # Model name migrations mapping old names to new names self.model_migrations = { # Legacy model names to current format "claude-3-sonnet": "claude-3-sonnet-20240229", "claude-3-opus": "claude-3-opus-20240229", "claude-3-haiku": "claude-3-haiku-20240307", "claude-3.5-sonnet": "claude-3-5-sonnet-20241022", "claude-3.5-haiku": "claude-3-5-haiku-20241022", # Handle common variations "claude-sonnet": "claude-3-sonnet-20240229", "claude-opus": "claude-3-opus-20240229", "claude-haiku": "claude-3-haiku-20240307", "sonnet": "claude-3-5-sonnet-20241022", "opus": "claude-3-opus-20240229", "haiku": "claude-3-haiku-20240307", } # Default values for new fields self.default_values = { 'claude': { 'api_url': "https://api.anthropic.com", 'model': "claude-3-5-sonnet-20241022", 'max_tokens': 4096, 'temperature': 0.7 }, 'journal': { 'daily_notes_folder': "Daily", 'date_format': "YYYY-MM-DD", 'file_extension': ".md" }, 'output': { 'experiences_folder': "Knowledge/Experiences", 'lessons_folder': "Knowledge/Lessons", 'tasks_folder': "Tasks/Daily", 'problems_folder': "Knowledge/Problems", 'achievements_folder': "Knowledge/Achievements", 'improvements_folder': "Knowledge/Improvements" }, 'analysis': { 'categories': [], 'extraction_rules': {} }, 'logging': { 'level': "INFO", 'file': "logs/journal_organizer.log" } } def migrate_claude_config(self, config_dict: Dict[str, Any]) -> Dict[str, Any]: """ Migrate legacy Claude configuration to new format Args: config_dict: Configuration dictionary to migrate Returns: Migrated configuration dictionary """ # Create a copy to avoid modifying the original migrated_config = config_dict.copy() # Ensure claude section exists if 'claude' not in migrated_config: migrated_config['claude'] = {} self.logger.info("Created missing 'claude' configuration section") claude_config = migrated_config['claude'] migration_actions = [] # Add default api_url if not present if 'api_url' not in claude_config: claude_config['api_url'] = self.default_values['claude']['api_url'] migration_actions.append(f"Added default api_url: {claude_config['api_url']}") # Ensure model has a default value if 'model' not in claude_config: claude_config['model'] = self.default_values['claude']['model'] migration_actions.append(f"Added default model: {claude_config['model']}") else: # Migrate old model names to new format if needed old_model = claude_config['model'] if old_model in self.model_migrations: new_model = self.model_migrations[old_model] claude_config['model'] = new_model migration_actions.append(f"Migrated model '{old_model}' to '{new_model}'") # Add other default values if missing if 'max_tokens' not in claude_config: claude_config['max_tokens'] = self.default_values['claude']['max_tokens'] migration_actions.append(f"Added default max_tokens: {claude_config['max_tokens']}") if 'temperature' not in claude_config: claude_config['temperature'] = self.default_values['claude']['temperature'] migration_actions.append(f"Added default temperature: {claude_config['temperature']}") # Log migration actions if migration_actions: self.logger.info("Claude configuration migration completed:") for action in migration_actions: self.logger.info(f" - {action}") return migrated_config def migrate_configuration(self, config_dict: Dict[str, Any]) -> Dict[str, Any]: """ Migrate complete configuration from legacy format to new format Args: config_dict: Configuration dictionary to migrate Returns: Migrated configuration dictionary """ migrated_config = config_dict.copy() all_migration_actions = [] # Migrate Claude configuration migrated_config = self.migrate_claude_config(migrated_config) # Migrate other sections if needed migrated_config, journal_actions = self._migrate_journal_config(migrated_config) all_migration_actions.extend(journal_actions) migrated_config, output_actions = self._migrate_output_config(migrated_config) all_migration_actions.extend(output_actions) migrated_config, analysis_actions = self._migrate_analysis_config(migrated_config) all_migration_actions.extend(analysis_actions) migrated_config, logging_actions = self._migrate_logging_config(migrated_config) all_migration_actions.extend(logging_actions) # Log overall migration summary if all_migration_actions: self.logger.info(f"Configuration migration completed with {len(all_migration_actions)} changes") self._log_migration_summary(all_migration_actions) else: self.logger.debug("No configuration migration needed - all fields are up to date") return migrated_config def _migrate_journal_config(self, config_dict: Dict[str, Any]) -> tuple[Dict[str, Any], List[str]]: """Migrate journal configuration section""" migrated_config = config_dict.copy() actions = [] if 'journal' not in migrated_config: migrated_config['journal'] = self.default_values['journal'].copy() actions.append("Created missing 'journal' configuration section with defaults") else: journal_config = migrated_config['journal'] # Add missing fields with defaults for field, default_value in self.default_values['journal'].items(): if field not in journal_config: journal_config[field] = default_value actions.append(f"Added default journal.{field}: {default_value}") return migrated_config, actions def _migrate_output_config(self, config_dict: Dict[str, Any]) -> tuple[Dict[str, Any], List[str]]: """Migrate output configuration section""" migrated_config = config_dict.copy() actions = [] if 'output' not in migrated_config: migrated_config['output'] = self.default_values['output'].copy() actions.append("Created missing 'output' configuration section with defaults") else: output_config = migrated_config['output'] # Add missing fields with defaults for field, default_value in self.default_values['output'].items(): if field not in output_config: output_config[field] = default_value actions.append(f"Added default output.{field}: {default_value}") return migrated_config, actions def _migrate_analysis_config(self, config_dict: Dict[str, Any]) -> tuple[Dict[str, Any], List[str]]: """Migrate analysis configuration section""" migrated_config = config_dict.copy() actions = [] if 'analysis' not in migrated_config: migrated_config['analysis'] = self.default_values['analysis'].copy() actions.append("Created missing 'analysis' configuration section with defaults") else: analysis_config = migrated_config['analysis'] # Add missing fields with defaults for field, default_value in self.default_values['analysis'].items(): if field not in analysis_config: analysis_config[field] = default_value actions.append(f"Added default analysis.{field}: {default_value}") return migrated_config, actions def _migrate_logging_config(self, config_dict: Dict[str, Any]) -> tuple[Dict[str, Any], List[str]]: """Migrate logging configuration section""" migrated_config = config_dict.copy() actions = [] if 'logging' not in migrated_config: migrated_config['logging'] = self.default_values['logging'].copy() actions.append("Created missing 'logging' configuration section with defaults") else: logging_config = migrated_config['logging'] # Add missing fields with defaults for field, default_value in self.default_values['logging'].items(): if field not in logging_config: logging_config[field] = default_value actions.append(f"Added default logging.{field}: {default_value}") return migrated_config, actions def _log_migration_summary(self, actions: List[str]) -> None: """Log a summary of migration actions taken""" self.logger.info("Migration summary:") for action in actions: self.logger.info(f" - {action}") # Provide helpful information about new features self.logger.info("") self.logger.info("New configuration options are now available:") self.logger.info(" - claude.api_url: Configure custom Claude API endpoints") self.logger.info(" - claude.model: Enhanced model validation with suggestions") self.logger.info(" - Environment variable support: Use ${VAR} and ${VAR:-default} patterns") self.logger.info(" - See config.example.yaml for complete configuration examples") def check_migration_needed(self, config_dict: Dict[str, Any]) -> bool: """ Check if configuration needs migration Args: config_dict: Configuration dictionary to check Returns: True if migration is needed, False otherwise """ # Check if Claude section needs migration claude_config = config_dict.get('claude', {}) # Check for missing new fields if 'api_url' not in claude_config: return True # Check for old model names that need migration model = claude_config.get('model', '') if model in self.model_migrations: return True # Check for missing other sections required_sections = ['journal', 'output', 'analysis', 'logging'] for section in required_sections: if section not in config_dict: return True # Check for missing fields in existing sections section_config = config_dict[section] default_fields = self.default_values.get(section, {}) for field in default_fields: if field not in section_config: return True return False def get_migration_preview(self, config_dict: Dict[str, Any]) -> List[str]: """ Get a preview of what migration actions would be taken Args: config_dict: Configuration dictionary to analyze Returns: List of migration actions that would be taken """ preview_actions = [] # Check Claude configuration claude_config = config_dict.get('claude', {}) if 'api_url' not in claude_config: preview_actions.append(f"Would add default api_url: {self.default_values['claude']['api_url']}") if 'model' not in claude_config: preview_actions.append(f"Would add default model: {self.default_values['claude']['model']}") elif claude_config['model'] in self.model_migrations: old_model = claude_config['model'] new_model = self.model_migrations[old_model] preview_actions.append(f"Would migrate model '{old_model}' to '{new_model}'") # Check other sections for section_name, section_defaults in self.default_values.items(): if section_name == 'claude': continue # Already handled above if section_name not in config_dict: preview_actions.append(f"Would create missing '{section_name}' section with defaults") else: section_config = config_dict[section_name] for field, default_value in section_defaults.items(): if field not in section_config: preview_actions.append(f"Would add default {section_name}.{field}: {default_value}") return preview_actions def get_supported_model_names(self) -> List[str]: """ Get list of all supported model names (both old and new) Returns: List of supported model names """ # Current valid models current_models = [ "claude-3-opus-20240229", "claude-3-sonnet-20240229", "claude-3-haiku-20240307", "claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022", "claude-3-opus-latest", "claude-3-sonnet-latest", "claude-3-haiku-latest", "claude-3-5-sonnet-latest", "claude-3-5-haiku-latest" ] # Legacy models that will be migrated legacy_models = list(self.model_migrations.keys()) return current_models + legacy_models