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,333 @@
|
||||
"""
|
||||
配置管理模块
|
||||
负责加载和管理系统的所有配置参数
|
||||
Enhanced with Pydantic validation and comprehensive error handling
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
from .dependency_manager import get_dependency_manager
|
||||
|
||||
# Import the new validation framework
|
||||
from .config_validation import (
|
||||
SystemConfig, ConfigurationValidator,
|
||||
ObsidianConfig as PydanticObsidianConfig,
|
||||
ClaudeAPIConfig as PydanticClaudeAPIConfig,
|
||||
JournalConfig as PydanticJournalConfig,
|
||||
OutputConfig as PydanticOutputConfig,
|
||||
AnalysisConfig as PydanticAnalysisConfig,
|
||||
LoggingConfig as PydanticLoggingConfig
|
||||
)
|
||||
|
||||
# Try to import yaml with graceful degradation
|
||||
dependency_manager = get_dependency_manager()
|
||||
yaml = dependency_manager.get_module('yaml')
|
||||
|
||||
|
||||
@dataclass
|
||||
class ObsidianConfig:
|
||||
"""Obsidian 配置"""
|
||||
|
||||
vault_path: str
|
||||
rest_api_url: str
|
||||
rest_api_key: str
|
||||
verify_ssl: bool = False
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Validate configuration after initialization"""
|
||||
if not self.vault_path or not self.vault_path.strip():
|
||||
raise ValueError("vault_path cannot be empty")
|
||||
if not self.rest_api_key or not self.rest_api_key.strip():
|
||||
raise ValueError("rest_api_key cannot be empty")
|
||||
if not self.rest_api_url or not self.rest_api_url.strip():
|
||||
raise ValueError("rest_api_url cannot be empty")
|
||||
if not self.rest_api_url.startswith(("http://", "https://")):
|
||||
raise ValueError("rest_api_url must start with http:// or https://")
|
||||
|
||||
|
||||
@dataclass
|
||||
class ClaudeConfig:
|
||||
"""Claude API 配置"""
|
||||
|
||||
api_key: str
|
||||
model: str = "claude-3-5-sonnet-20241022"
|
||||
api_url: str = "https://api.anthropic.com"
|
||||
max_tokens: int = 4096
|
||||
temperature: float = 0.7
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Validate configuration after initialization"""
|
||||
if not self.api_key or not self.api_key.strip():
|
||||
raise ValueError("api_key cannot be empty")
|
||||
if self.max_tokens <= 0:
|
||||
raise ValueError("max_tokens must be positive")
|
||||
if not 0.0 <= self.temperature <= 2.0:
|
||||
raise ValueError("temperature must be between 0.0 and 2.0")
|
||||
if not self.api_url or not self.api_url.strip():
|
||||
raise ValueError("api_url cannot be empty")
|
||||
if not self.api_url.startswith(("http://", "https://")):
|
||||
raise ValueError("api_url must start with http:// or https://")
|
||||
|
||||
|
||||
@dataclass
|
||||
class JournalConfig:
|
||||
"""日记配置"""
|
||||
|
||||
daily_notes_folder: str
|
||||
date_format: str = "YYYY-MM-DD"
|
||||
file_extension: str = ".md"
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Validate configuration after initialization"""
|
||||
if not self.daily_notes_folder or not self.daily_notes_folder.strip():
|
||||
raise ValueError("daily_notes_folder cannot be empty")
|
||||
if not self.file_extension.startswith("."):
|
||||
raise ValueError("file_extension must start with a dot")
|
||||
|
||||
|
||||
@dataclass
|
||||
class OutputConfig:
|
||||
"""输出配置"""
|
||||
|
||||
experiences_folder: str
|
||||
lessons_folder: str
|
||||
tasks_folder: str
|
||||
problems_folder: str
|
||||
achievements_folder: str
|
||||
improvements_folder: str
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Validate configuration after initialization"""
|
||||
folders = [
|
||||
self.experiences_folder,
|
||||
self.lessons_folder,
|
||||
self.tasks_folder,
|
||||
self.problems_folder,
|
||||
self.achievements_folder,
|
||||
self.improvements_folder,
|
||||
]
|
||||
for folder in folders:
|
||||
if not folder or not folder.strip():
|
||||
raise ValueError("All folder paths must be non-empty")
|
||||
|
||||
|
||||
@dataclass
|
||||
class AnalysisConfig:
|
||||
"""分析配置"""
|
||||
|
||||
categories: List[str] = field(default_factory=list)
|
||||
extraction_rules: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Validate configuration after initialization"""
|
||||
if not isinstance(self.categories, list):
|
||||
raise ValueError("categories must be a list")
|
||||
if not isinstance(self.extraction_rules, dict):
|
||||
raise ValueError("extraction_rules must be a dictionary")
|
||||
|
||||
|
||||
@dataclass
|
||||
class LoggingConfig:
|
||||
"""日志配置"""
|
||||
|
||||
level: str = "INFO"
|
||||
file: str = "logs/journal_organizer.log"
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Validate configuration after initialization"""
|
||||
valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
||||
if self.level not in valid_levels:
|
||||
raise ValueError(f"level must be one of {valid_levels}")
|
||||
if not self.file or not self.file.strip():
|
||||
raise ValueError("file path cannot be empty")
|
||||
|
||||
|
||||
class Config:
|
||||
"""系统配置管理器 - Enhanced with Pydantic validation"""
|
||||
|
||||
def __init__(self, config_file: Optional[str] = None):
|
||||
"""
|
||||
初始化配置
|
||||
|
||||
Args:
|
||||
config_file: 配置文件路径,如果为 None 则使用默认位置
|
||||
"""
|
||||
self.config_file = config_file or self._get_default_config_path()
|
||||
self.validator = ConfigurationValidator()
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# Load and validate configuration using Pydantic
|
||||
try:
|
||||
self.system_config = self.validator.load_and_validate_config(self.config_file)
|
||||
except (FileNotFoundError, ValueError) as e:
|
||||
self.logger.error(f"Configuration error: {e}")
|
||||
raise
|
||||
|
||||
# Create legacy-compatible attributes
|
||||
self._create_legacy_attributes()
|
||||
|
||||
# Perform additional validations
|
||||
self._perform_additional_validations()
|
||||
|
||||
def _create_legacy_attributes(self) -> None:
|
||||
"""Create legacy-compatible attributes from Pydantic models"""
|
||||
# Convert Pydantic models to legacy dataclass format for backward compatibility
|
||||
self.obsidian = ObsidianConfig(
|
||||
vault_path=self.system_config.obsidian.vault_path,
|
||||
rest_api_url=self.system_config.obsidian.rest_api.url,
|
||||
rest_api_key=self.system_config.obsidian.rest_api.api_key,
|
||||
verify_ssl=self.system_config.obsidian.rest_api.verify_ssl
|
||||
)
|
||||
|
||||
self.claude = ClaudeConfig(
|
||||
api_key=self.system_config.claude.api_key,
|
||||
model=self.system_config.claude.model,
|
||||
api_url=self.system_config.claude.api_url,
|
||||
max_tokens=self.system_config.claude.max_tokens,
|
||||
temperature=self.system_config.claude.temperature
|
||||
)
|
||||
|
||||
self.journal = JournalConfig(
|
||||
daily_notes_folder=self.system_config.journal.daily_notes_folder,
|
||||
date_format=self.system_config.journal.date_format,
|
||||
file_extension=self.system_config.journal.file_extension
|
||||
)
|
||||
|
||||
self.output = OutputConfig(
|
||||
experiences_folder=self.system_config.output.experiences_folder,
|
||||
lessons_folder=self.system_config.output.lessons_folder,
|
||||
tasks_folder=self.system_config.output.tasks_folder,
|
||||
problems_folder=self.system_config.output.problems_folder,
|
||||
achievements_folder=self.system_config.output.achievements_folder,
|
||||
improvements_folder=self.system_config.output.improvements_folder
|
||||
)
|
||||
|
||||
self.analysis = AnalysisConfig(
|
||||
categories=self.system_config.analysis.categories,
|
||||
extraction_rules=self.system_config.analysis.extraction_rules
|
||||
)
|
||||
|
||||
self.logging = LoggingConfig(
|
||||
level=self.system_config.logging.level,
|
||||
file=self.system_config.logging.file
|
||||
)
|
||||
|
||||
def _perform_additional_validations(self) -> None:
|
||||
"""Perform additional validations and provide helpful guidance"""
|
||||
# Validate API keys and provide setup instructions
|
||||
api_key_issues = self.validator.validate_api_keys(self.system_config)
|
||||
if api_key_issues:
|
||||
error_msg = "API Key Configuration Issues:\n" + "\n\n".join(api_key_issues)
|
||||
self.logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
# Validate file paths
|
||||
path_issues = self.validator.validate_file_paths(self.system_config)
|
||||
if path_issues:
|
||||
error_msg = "File Path Issues:\n" + "\n".join(path_issues)
|
||||
self.logger.warning(error_msg)
|
||||
# Don't raise error for path issues, just warn
|
||||
|
||||
def get_environment_variable_help(self) -> str:
|
||||
"""Get help for setting up environment variables"""
|
||||
return self.validator.get_environment_variable_help()
|
||||
|
||||
def get_path_setup_help(self) -> str:
|
||||
"""Get help for setting up file paths"""
|
||||
return self.validator.get_path_setup_help()
|
||||
|
||||
def _get_default_config_path(self) -> str:
|
||||
"""获取默认配置文件路径"""
|
||||
# First check current directory
|
||||
current_dir_config = Path("config.yaml")
|
||||
if current_dir_config.exists():
|
||||
return str(current_dir_config)
|
||||
|
||||
# Then check user home directory
|
||||
config_dir = Path.home() / ".journal_organizer"
|
||||
config_dir.mkdir(exist_ok=True)
|
||||
return str(config_dir / "config.yaml")
|
||||
|
||||
def get_validation_errors(self) -> List[str]:
|
||||
"""Get any validation errors or warnings"""
|
||||
errors = []
|
||||
|
||||
try:
|
||||
# Re-validate to get current status
|
||||
api_key_issues = self.validator.validate_api_keys(self.system_config)
|
||||
errors.extend(api_key_issues)
|
||||
|
||||
path_issues = self.validator.validate_file_paths(self.system_config)
|
||||
errors.extend(path_issues)
|
||||
|
||||
except Exception as e:
|
||||
errors.append(f"Validation error: {e}")
|
||||
|
||||
return errors
|
||||
|
||||
def reload_config(self) -> None:
|
||||
"""Reload configuration from file"""
|
||||
try:
|
||||
self.system_config = self.validator.load_and_validate_config(self.config_file)
|
||||
self._create_legacy_attributes()
|
||||
self._perform_additional_validations()
|
||||
self.logger.info("Configuration reloaded successfully")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to reload configuration: {e}")
|
||||
raise
|
||||
|
||||
def get_daily_note_path(self, date_str: str) -> str:
|
||||
"""
|
||||
获取指定日期的日记文件路径
|
||||
|
||||
Args:
|
||||
date_str: 日期字符串,格式应与配置中的 date_format 一致
|
||||
|
||||
Returns:
|
||||
日记文件的相对路径
|
||||
"""
|
||||
daily_path = (
|
||||
Path(self.journal.daily_notes_folder)
|
||||
/ f"{date_str}{self.journal.file_extension}"
|
||||
)
|
||||
return str(daily_path)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""转换为字典(不包含敏感信息)"""
|
||||
return {
|
||||
"obsidian": {
|
||||
"vault_path": self.obsidian.vault_path,
|
||||
"rest_api_url": self.obsidian.rest_api_url,
|
||||
},
|
||||
"claude": {
|
||||
"model": self.claude.model,
|
||||
"api_url": self.claude.api_url,
|
||||
"max_tokens": self.claude.max_tokens,
|
||||
"temperature": self.claude.temperature,
|
||||
},
|
||||
"journal": {
|
||||
"daily_notes_folder": self.journal.daily_notes_folder,
|
||||
"date_format": self.journal.date_format,
|
||||
"file_extension": self.journal.file_extension,
|
||||
},
|
||||
"output": {
|
||||
"experiences_folder": self.output.experiences_folder,
|
||||
"lessons_folder": self.output.lessons_folder,
|
||||
"tasks_folder": self.output.tasks_folder,
|
||||
"problems_folder": self.output.problems_folder,
|
||||
"achievements_folder": self.output.achievements_folder,
|
||||
"improvements_folder": self.output.improvements_folder,
|
||||
},
|
||||
"analysis": {
|
||||
"categories": self.analysis.categories,
|
||||
},
|
||||
"logging": {
|
||||
"level": self.logging.level,
|
||||
"file": self.logging.file,
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user