import json import os from typing import Any, Dict, List from pydantic import BaseModel, Field class PersonaConfig(BaseModel): role: str tone: str style: str boundaries: List[str] = [] class MemoryConfig(BaseModel): session_turns: int persistent_store: str summarize_after: int class ChatConfig(BaseModel): streaming: bool max_response_tokens: int default_temperature: float allow_temperature_override: bool class CompanionConfig(BaseModel): name: str persona: PersonaConfig memory: MemoryConfig chat: ChatConfig class IndexingConfig(BaseModel): auto_sync: bool auto_sync_interval_minutes: int watch_fs_events: bool file_patterns: List[str] deny_dirs: List[str] deny_patterns: List[str] class ChunkingRule(BaseModel): strategy: str chunk_size: int chunk_overlap: int section_tags: List[str] = [] class VaultConfig(BaseModel): path: str indexing: IndexingConfig chunking_rules: Dict[str, ChunkingRule] = {} class EmbeddingConfig(BaseModel): provider: str model: str base_url: str dimensions: int batch_size: int class VectorStoreConfig(BaseModel): type: str path: str class HybridSearchConfig(BaseModel): enabled: bool keyword_weight: float semantic_weight: float class FiltersConfig(BaseModel): date_range_enabled: bool tag_filter_enabled: bool directory_filter_enabled: bool class SearchConfig(BaseModel): default_top_k: int max_top_k: int similarity_threshold: float hybrid_search: HybridSearchConfig filters: FiltersConfig class RagConfig(BaseModel): embedding: EmbeddingConfig vector_store: VectorStoreConfig search: SearchConfig class InferenceConfig(BaseModel): backend: str model_path: str context_length: int gpu_layers: int batch_size: int threads: int class FineTuningConfig(BaseModel): base_model: str output_dir: str lora_rank: int lora_alpha: int learning_rate: float batch_size: int gradient_accumulation_steps: int num_epochs: int warmup_steps: int save_steps: int eval_steps: int training_data_path: str validation_split: float class RetrainScheduleConfig(BaseModel): auto_reminder: bool default_interval_days: int reminder_channels: List[str] = [] class ModelConfig(BaseModel): inference: InferenceConfig fine_tuning: FineTuningConfig retrain_schedule: RetrainScheduleConfig class AuthConfig(BaseModel): enabled: bool class ApiConfig(BaseModel): host: str port: int cors_origins: List[str] = [] auth: AuthConfig class WebFeaturesConfig(BaseModel): streaming: bool citations: bool source_preview: bool class WebConfig(BaseModel): enabled: bool theme: str features: WebFeaturesConfig class CliConfig(BaseModel): enabled: bool rich_output: bool class UiConfig(BaseModel): web: WebConfig cli: CliConfig class LoggingConfig(BaseModel): level: str file: str max_size_mb: int backup_count: int class SecurityConfig(BaseModel): local_only: bool vault_path_traversal_check: bool sensitive_content_detection: bool sensitive_patterns: List[str] = [] require_confirmation_for_external_apis: bool class Config(BaseModel): companion: CompanionConfig vault: VaultConfig rag: RagConfig model: ModelConfig api: ApiConfig ui: UiConfig logging: LoggingConfig security: SecurityConfig def expand_tilde_recursive(obj: Any) -> Any: """Recursively expand ~/ in string values.""" if isinstance(obj, str) and obj.startswith("~/"): return os.path.expanduser(obj) elif isinstance(obj, dict): return {k: expand_tilde_recursive(v) for k, v in obj.items()} elif isinstance(obj, list): return [expand_tilde_recursive(item) for item in obj] return obj def load_config(path: str) -> Config: """Load configuration from a JSON file with tilde expansion.""" with open(path, "r", encoding="utf-8") as f: raw_data = json.load(f) # Recursively expand tilde paths in strings expanded_data = expand_tilde_recursive(raw_data) return Config.model_validate(expanded_data)