130 lines
4.1 KiB
Python
130 lines
4.1 KiB
Python
"""
|
|
config.py — Configuration loading and validation.
|
|
|
|
Loaded once at startup. Fails fast if required variables are missing.
|
|
All other modules import `settings` from here.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
|
|
from dotenv import load_dotenv
|
|
|
|
# Load .env from the project root (one level above server/)
|
|
_env_path = Path(__file__).parent.parent / ".env"
|
|
load_dotenv(_env_path)
|
|
|
|
|
|
_PROJECT_ROOT = Path(__file__).parent.parent
|
|
|
|
|
|
def _extract_agent_name(fallback: str = "Jarvis") -> str:
|
|
"""Read agent name from SOUL.md. Looks for 'You are **Name**', then the # heading."""
|
|
try:
|
|
soul = (_PROJECT_ROOT / "SOUL.md").read_text(encoding="utf-8")
|
|
except FileNotFoundError:
|
|
return fallback
|
|
# Primary: "You are **Name**"
|
|
m = re.search(r"You are \*\*([^*]+)\*\*", soul)
|
|
if m:
|
|
return m.group(1).strip()
|
|
# Fallback: first "# Name" heading, dropping anything after " — "
|
|
for line in soul.splitlines():
|
|
if line.startswith("# "):
|
|
name = line[2:].split("—")[0].strip()
|
|
if name:
|
|
return name
|
|
return fallback
|
|
|
|
|
|
def _require(key: str) -> str:
|
|
"""Get a required environment variable, fail fast if missing."""
|
|
value = os.getenv(key)
|
|
if not value:
|
|
print(f"[aide] FATAL: Required environment variable '{key}' is not set.", file=sys.stderr)
|
|
print(f"[aide] Copy .env.example to .env and fill in your values.", file=sys.stderr)
|
|
sys.exit(1)
|
|
return value
|
|
|
|
|
|
def _optional(key: str, default: str = "") -> str:
|
|
return os.getenv(key, default)
|
|
|
|
|
|
@dataclass
|
|
class Settings:
|
|
# Required
|
|
db_master_password: str
|
|
|
|
# AI provider selection — keys are stored in the DB, not here
|
|
default_provider: str = "anthropic" # "anthropic", "openrouter", or "openai"
|
|
default_model: str = "" # Empty = use provider's default model
|
|
|
|
# Optional with defaults
|
|
port: int = 8080
|
|
max_tool_calls: int = 20
|
|
max_autonomous_runs_per_hour: int = 10
|
|
timezone: str = "Europe/Oslo"
|
|
|
|
# Agent identity — derived from SOUL.md at startup, fallback if file absent
|
|
agent_name: str = "Jarvis"
|
|
|
|
# Model selection — empty list triggers auto-discovery at runtime
|
|
available_models: list[str] = field(default_factory=list)
|
|
default_chat_model: str = ""
|
|
|
|
# Database
|
|
aide_db_url: str = ""
|
|
|
|
|
|
def _load() -> Settings:
|
|
master_password = _require("DB_MASTER_PASSWORD")
|
|
|
|
default_provider = _optional("DEFAULT_PROVIDER", "anthropic").lower()
|
|
default_model = _optional("DEFAULT_MODEL", "")
|
|
|
|
_known_providers = {"anthropic", "openrouter", "openai"}
|
|
if default_provider not in _known_providers:
|
|
print(f"[aide] FATAL: Unknown DEFAULT_PROVIDER '{default_provider}'. Use 'anthropic', 'openrouter', or 'openai'.", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
port = int(_optional("PORT", "8080"))
|
|
max_tool_calls = int(_optional("MAX_TOOL_CALLS", "20"))
|
|
max_runs = int(_optional("MAX_AUTONOMOUS_RUNS_PER_HOUR", "10"))
|
|
timezone = _optional("TIMEZONE", "Europe/Oslo")
|
|
|
|
def _normalize_model(m: str) -> str:
|
|
"""Prepend default_provider if model has no provider prefix."""
|
|
parts = m.split(":", 1)
|
|
if len(parts) == 2 and parts[0] in _known_providers:
|
|
return m
|
|
return f"{default_provider}:{m}"
|
|
|
|
available_models: list[str] = [] # unused; kept for backward compat
|
|
default_chat_model_raw = _optional("DEFAULT_CHAT_MODEL", "")
|
|
default_chat_model = _normalize_model(default_chat_model_raw) if default_chat_model_raw else ""
|
|
|
|
aide_db_url = _require("AIDE_DB_URL")
|
|
|
|
return Settings(
|
|
agent_name=_extract_agent_name(),
|
|
db_master_password=master_password,
|
|
default_provider=default_provider,
|
|
default_model=default_model,
|
|
port=port,
|
|
max_tool_calls=max_tool_calls,
|
|
max_autonomous_runs_per_hour=max_runs,
|
|
timezone=timezone,
|
|
available_models=available_models,
|
|
default_chat_model=default_chat_model,
|
|
aide_db_url=aide_db_url,
|
|
)
|
|
|
|
|
|
# Module-level singleton — import this everywhere
|
|
settings = _load()
|