CheddahBot/cheddahbot/config.py

201 lines
7.0 KiB
Python

"""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