"""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 = "internal review" in_progress_status: str = "in progress" task_type_field_name: str = "Work Category" default_auto_execute: bool = False skill_map: dict = field(default_factory=dict) enabled: bool = False @dataclass class PressAdvantageConfig: api_key: str = "" base_url: str = "https://app.pressadvantage.com" @dataclass class EmailConfig: smtp_host: str = "smtp.gmail.com" smtp_port: int = 465 username: str = "" password: str = "" default_to: str = "" enabled: bool = False @dataclass class AgentConfig: """Per-agent configuration for multi-agent support.""" name: str = "default" display_name: str = "CheddahBot" personality_file: str = "" # path to SOUL-like .md file, empty = default model: str = "" # model override, empty = use global chat_model tools: list[str] | None = None # tool name whitelist, None = all skills: list[str] | None = None # skill name filter, None = auto memory_scope: str = "" # memory namespace, empty = shared @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) press_advantage: PressAdvantageConfig = field(default_factory=PressAdvantageConfig) email: EmailConfig = field(default_factory=EmailConfig) agents: list[AgentConfig] = field(default_factory=lambda: [AgentConfig()]) # 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) if "press_advantage" in data and isinstance(data["press_advantage"], dict): for k, v in data["press_advantage"].items(): if hasattr(cfg.press_advantage, k): setattr(cfg.press_advantage, k, v) if "email" in data and isinstance(data["email"], dict): for k, v in data["email"].items(): if hasattr(cfg.email, k): setattr(cfg.email, k, v) # Multi-agent configs if "agents" in data and isinstance(data["agents"], list): cfg.agents = [] for agent_data in data["agents"]: if isinstance(agent_data, dict): ac = AgentConfig() for k, v in agent_data.items(): if hasattr(ac, k): setattr(ac, k, v) cfg.agents.append(ac) # Ensure at least one agent if not cfg.agents: cfg.agents = [AgentConfig()] # 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) # Press Advantage env var override if key := os.getenv("PRESS_ADVANTAGE_API"): cfg.press_advantage.api_key = key # Email env var overrides if gmail_user := os.getenv("GMAIL_USERNAME"): cfg.email.username = gmail_user if gmail_pass := os.getenv("GMAIL_APP_PASSWORD"): cfg.email.password = gmail_pass if default_to := os.getenv("EMAIL_DEFAULT_TO"): cfg.email.default_to = default_to cfg.email.enabled = bool(cfg.email.username and cfg.email.password) # 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