CheddahBot/cheddahbot/config.py

265 lines
9.5 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"
pr_review_status: str = "pr needs review"
in_progress_status: str = "in progress"
automation_status: str = "automation underway"
error_status: str = "error"
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 LinkBuildingConfig:
blm_dir: str = "E:/dev/Big-Link-Man"
watch_folder: str = "" # empty = disabled
watch_interval_minutes: int = 60
default_branded_plus_ratio: float = 0.7
@dataclass
class AutoCoraConfig:
jobs_dir: str = "//PennQnap1/SHARE1/AutoCora/jobs"
results_dir: str = "//PennQnap1/SHARE1/AutoCora/results"
poll_interval_minutes: int = 5
success_status: str = "running cora"
error_status: str = "error"
enabled: bool = False
cora_categories: list[str] = field(
default_factory=lambda: ["Content Creation", "On Page Optimization", "Link Building"]
)
cora_human_inbox: str = "" # e.g. "Z:/Cora-For-Human"
@dataclass
class ApiBudgetConfig:
monthly_limit: float = 20.00 # USD - alert when exceeded
alert_threshold: float = 0.8 # alert at 80% of limit
@dataclass
class ContentConfig:
cora_inbox: str = "" # e.g. "Z:/content-cora-inbox"
outline_dir: str = "" # e.g. "Z:/content-outlines"
company_capabilities_default: str = (
"All certifications and licenses need to be verified on the company's website."
)
@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)
link_building: LinkBuildingConfig = field(default_factory=LinkBuildingConfig)
autocora: AutoCoraConfig = field(default_factory=AutoCoraConfig)
api_budget: ApiBudgetConfig = field(default_factory=ApiBudgetConfig)
content: ContentConfig = field(default_factory=ContentConfig)
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)
if "link_building" in data and isinstance(data["link_building"], dict):
for k, v in data["link_building"].items():
if hasattr(cfg.link_building, k):
setattr(cfg.link_building, k, v)
if "autocora" in data and isinstance(data["autocora"], dict):
for k, v in data["autocora"].items():
if hasattr(cfg.autocora, k):
setattr(cfg.autocora, k, v)
if "api_budget" in data and isinstance(data["api_budget"], dict):
for k, v in data["api_budget"].items():
if hasattr(cfg.api_budget, k):
setattr(cfg.api_budget, k, v)
if "content" in data and isinstance(data["content"], dict):
for k, v in data["content"].items():
if hasattr(cfg.content, k):
setattr(cfg.content, 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)
# Link Building env var overrides
if blm_dir := os.getenv("BLM_DIR"):
cfg.link_building.blm_dir = blm_dir
# 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