"""Configuration loader: env vars → config.yaml → defaults.""" from __future__ import annotations import os from dataclasses import dataclass, field from pathlib import Path import yaml from dotenv import load_dotenv ROOT_DIR = Path(__file__).resolve().parent.parent load_dotenv(ROOT_DIR / ".env") @dataclass class MemoryConfig: max_context_messages: int = 50 flush_threshold: int = 40 embedding_model: str = "all-MiniLM-L6-v2" search_top_k: int = 5 @dataclass class SchedulerConfig: heartbeat_interval_minutes: int = 30 poll_interval_seconds: int = 60 @dataclass class ShellConfig: blocked_commands: list[str] = field(default_factory=lambda: ["rm -rf /", "format", ":(){:|:&};:"]) require_approval: bool = False @dataclass class ClickUpConfig: api_token: str = "" workspace_id: str = "" space_id: str = "" poll_interval_minutes: int = 20 poll_statuses: list[str] = field(default_factory=lambda: ["to do"]) review_status: str = "review" in_progress_status: str = "in progress" task_type_field_name: str = "Task Type" default_auto_execute: bool = False skill_map: dict = field(default_factory=dict) enabled: bool = False @dataclass class Config: chat_model: str = "openai/gpt-4o-mini" default_model: str = "claude-sonnet-4.5" host: str = "0.0.0.0" port: int = 7860 ollama_url: str = "http://localhost:11434" lmstudio_url: str = "http://localhost:1234" openrouter_api_key: str = "" memory: MemoryConfig = field(default_factory=MemoryConfig) scheduler: SchedulerConfig = field(default_factory=SchedulerConfig) shell: ShellConfig = field(default_factory=ShellConfig) clickup: ClickUpConfig = field(default_factory=ClickUpConfig) # Derived paths root_dir: Path = field(default_factory=lambda: ROOT_DIR) data_dir: Path = field(default_factory=lambda: ROOT_DIR / "data") identity_dir: Path = field(default_factory=lambda: ROOT_DIR / "identity") memory_dir: Path = field(default_factory=lambda: ROOT_DIR / "memory") skills_dir: Path = field(default_factory=lambda: ROOT_DIR / "skills") db_path: Path = field(default_factory=lambda: ROOT_DIR / "data" / "cheddahbot.db") def load_config() -> Config: """Load config from env vars → config.yaml → defaults.""" cfg = Config() # Load YAML if exists yaml_path = ROOT_DIR / "config.yaml" if yaml_path.exists(): with open(yaml_path) as f: data = yaml.safe_load(f) or {} for key in ("chat_model", "default_model", "host", "port", "ollama_url", "lmstudio_url"): if key in data: setattr(cfg, key, data[key]) if "memory" in data and isinstance(data["memory"], dict): for k, v in data["memory"].items(): if hasattr(cfg.memory, k): setattr(cfg.memory, k, v) if "scheduler" in data and isinstance(data["scheduler"], dict): for k, v in data["scheduler"].items(): if hasattr(cfg.scheduler, k): setattr(cfg.scheduler, k, v) if "shell" in data and isinstance(data["shell"], dict): for k, v in data["shell"].items(): if hasattr(cfg.shell, k): setattr(cfg.shell, k, v) if "clickup" in data and isinstance(data["clickup"], dict): for k, v in data["clickup"].items(): if hasattr(cfg.clickup, k): setattr(cfg.clickup, k, v) # Env var overrides (CHEDDAH_ prefix) cfg.openrouter_api_key = os.getenv("OPENROUTER_API_KEY", "") if cm := os.getenv("CHEDDAH_CHAT_MODEL"): cfg.chat_model = cm if m := os.getenv("CHEDDAH_DEFAULT_MODEL"): cfg.default_model = m if h := os.getenv("CHEDDAH_HOST"): cfg.host = h if p := os.getenv("CHEDDAH_PORT"): cfg.port = int(p) # ClickUp env var overrides if token := os.getenv("CLICKUP_API_TOKEN"): cfg.clickup.api_token = token if ws := os.getenv("CLICKUP_WORKSPACE_ID"): cfg.clickup.workspace_id = ws if sp := os.getenv("CLICKUP_SPACE_ID"): cfg.clickup.space_id = sp # Auto-enable if token is present cfg.clickup.enabled = bool(cfg.clickup.api_token) # Ensure data directories exist cfg.data_dir.mkdir(parents=True, exist_ok=True) (cfg.data_dir / "uploads").mkdir(exist_ok=True) (cfg.data_dir / "generated").mkdir(exist_ok=True) cfg.memory_dir.mkdir(parents=True, exist_ok=True) cfg.skills_dir.mkdir(parents=True, exist_ok=True) return cfg