Files
oai-web/server/config.py
2026-04-08 12:43:24 +02:00

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()