245 lines
8.7 KiB
Python
245 lines
8.7 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"
|
|
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 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)
|
|
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 "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
|