Removed the Config Manager Integration block and all 19 backward-compat variable re-exports (LLAMA_URL, CHESHIRE_CAT_URL, LANGUAGE_MODE, etc.) from config.py. These were dead code because: 1. Circular import: config.py tried to import config_manager at module level, but config_manager.py imports from config.py first, so HAS_CONFIG_MANAGER was always False and _get_config_value() was a no-op that always returned the static value. 2. Frozen snapshots: Even if the circular import worked, the values were assigned to module-level names at import time and never updated. Other modules importing 'from config import LLAMA_URL' would get a stale snapshot, not a live value. 3. Nothing imports them: The entire codebase uses globals.py for mutable runtime state, not these config.py copies. Only ERROR_WEBHOOK_URL was imported (by error_handler.py), so it is kept as a simple re-export from SECRETS. Also cleaned up unused imports: Any, field_validator. Japanese mode is NOT affected — LANGUAGE_MODE and JAPANESE_TEXT_MODEL live in globals.py and are untouched.
242 lines
7.7 KiB
Python
242 lines
7.7 KiB
Python
"""
|
|
Configuration management for Miku Discord Bot.
|
|
Uses Pydantic for type-safe configuration loading from:
|
|
- .env (secrets only)
|
|
- config.yaml (all other configuration)
|
|
"""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
from pydantic import BaseModel, Field
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
|
# ============================================
|
|
# Pydantic Models for Configuration
|
|
# ============================================
|
|
|
|
|
|
class ServicesConfig(BaseModel):
|
|
"""External service endpoint configuration"""
|
|
url: str = "http://llama-swap:8080"
|
|
amd_url: str = "http://llama-swap-amd:8080"
|
|
|
|
|
|
class CheshireCatConfig(BaseModel):
|
|
"""Cheshire Cat AI memory system configuration"""
|
|
url: str = "http://cheshire-cat:80"
|
|
timeout_seconds: int = Field(default=120, ge=1, le=600)
|
|
enabled: bool = True
|
|
|
|
|
|
class FaceDetectorConfig(BaseModel):
|
|
"""Face detection service configuration"""
|
|
startup_timeout_seconds: int = Field(default=60, ge=10, le=300)
|
|
|
|
|
|
class ModelsConfig(BaseModel):
|
|
"""AI model configuration"""
|
|
text: str = "llama3.1"
|
|
vision: str = "vision"
|
|
evil: str = "darkidol"
|
|
japanese: str = "swallow"
|
|
|
|
|
|
class DiscordConfig(BaseModel):
|
|
"""Discord bot configuration"""
|
|
language_mode: str = Field(default="english", pattern="^(english|japanese)$")
|
|
api_port: int = Field(default=3939, ge=1024, le=65535)
|
|
|
|
|
|
class AutonomousConfig(BaseModel):
|
|
"""Autonomous system configuration"""
|
|
debug_mode: bool = False
|
|
|
|
|
|
class VoiceConfig(BaseModel):
|
|
"""Voice chat configuration"""
|
|
debug_mode: bool = False
|
|
|
|
|
|
class MemoryConfig(BaseModel):
|
|
"""Memory and logging configuration"""
|
|
log_dir: str = "/app/memory/logs"
|
|
conversation_history_length: int = Field(default=5, ge=1, le=50)
|
|
|
|
|
|
class ServerConfig(BaseModel):
|
|
"""Server settings"""
|
|
host: str = "0.0.0.0"
|
|
log_level: str = Field(default="critical", pattern="^(debug|info|warning|error|critical)$")
|
|
|
|
|
|
class GPUConfig(BaseModel):
|
|
"""GPU configuration"""
|
|
prefer_amd: bool = False
|
|
amd_models_enabled: bool = True
|
|
|
|
|
|
class AppConfig(BaseModel):
|
|
"""Main application configuration"""
|
|
services: ServicesConfig = Field(default_factory=ServicesConfig)
|
|
cheshire_cat: CheshireCatConfig = Field(default_factory=CheshireCatConfig)
|
|
face_detector: FaceDetectorConfig = Field(default_factory=FaceDetectorConfig)
|
|
models: ModelsConfig = Field(default_factory=ModelsConfig)
|
|
discord: DiscordConfig = Field(default_factory=DiscordConfig)
|
|
autonomous: AutonomousConfig = Field(default_factory=AutonomousConfig)
|
|
voice: VoiceConfig = Field(default_factory=VoiceConfig)
|
|
memory: MemoryConfig = Field(default_factory=MemoryConfig)
|
|
server: ServerConfig = Field(default_factory=ServerConfig)
|
|
gpu: GPUConfig = Field(default_factory=GPUConfig)
|
|
|
|
|
|
class Secrets(BaseSettings):
|
|
"""
|
|
Secrets loaded from environment variables (.env file)
|
|
These are sensitive values that should never be committed to git
|
|
"""
|
|
model_config = SettingsConfigDict(
|
|
env_file=".env",
|
|
env_file_encoding="utf-8",
|
|
env_prefix="", # No prefix for env vars
|
|
extra="ignore" # Ignore extra env vars
|
|
)
|
|
|
|
# Discord
|
|
discord_bot_token: str = Field(..., description="Discord bot token")
|
|
|
|
# API Keys
|
|
cheshire_cat_api_key: str = Field(default="", description="Cheshire Cat API key (empty if no auth)")
|
|
|
|
# Error Reporting
|
|
error_webhook_url: Optional[str] = Field(default=None, description="Discord webhook for error notifications")
|
|
|
|
# Owner
|
|
owner_user_id: int = Field(default=209381657369772032, description="Bot owner Discord user ID")
|
|
|
|
|
|
# ============================================
|
|
# Configuration Loader
|
|
# ============================================
|
|
|
|
|
|
def load_config(config_path: str = None) -> AppConfig:
|
|
"""
|
|
Load configuration from YAML file.
|
|
|
|
Args:
|
|
config_path: Path to config.yaml (defaults to ../config.yaml from bot directory)
|
|
|
|
Returns:
|
|
AppConfig instance
|
|
"""
|
|
import yaml
|
|
|
|
if config_path is None:
|
|
# Default: try Docker path first, then fall back to relative path
|
|
# In Docker, config.yaml is mounted at /app/config.yaml
|
|
docker_config = Path("/app/config.yaml")
|
|
if docker_config.exists():
|
|
config_path = docker_config
|
|
else:
|
|
# Not in Docker, go up one level from bot/ directory
|
|
config_path = Path(__file__).parent.parent / "config.yaml"
|
|
|
|
config_file = Path(config_path)
|
|
|
|
if not config_file.exists():
|
|
# Fall back to default config if file doesn't exist
|
|
print(f"⚠️ Config file not found: {config_file}")
|
|
print("Using default configuration")
|
|
return AppConfig()
|
|
|
|
with open(config_file, "r") as f:
|
|
config_data = yaml.safe_load(f) or {}
|
|
|
|
return AppConfig(**config_data)
|
|
|
|
|
|
def load_secrets() -> Secrets:
|
|
"""
|
|
Load secrets from environment variables (.env file).
|
|
|
|
Returns:
|
|
Secrets instance
|
|
"""
|
|
return Secrets()
|
|
|
|
|
|
# ============================================
|
|
# Unified Configuration Instance
|
|
# ============================================
|
|
|
|
# Load configuration at module import time
|
|
CONFIG = load_config()
|
|
SECRETS = load_secrets()
|
|
|
|
# ============================================
|
|
# Re-exports for modules that import from config
|
|
# ============================================
|
|
# error_handler.py imports ERROR_WEBHOOK_URL from here
|
|
ERROR_WEBHOOK_URL = SECRETS.error_webhook_url
|
|
|
|
# ============================================
|
|
# Validation & Health Check
|
|
# ============================================
|
|
|
|
|
|
def validate_config() -> tuple[bool, list[str]]:
|
|
"""
|
|
Validate that all required configuration is present.
|
|
|
|
Returns:
|
|
Tuple of (is_valid, list_of_errors)
|
|
"""
|
|
errors = []
|
|
|
|
# Check secrets
|
|
if not SECRETS.discord_bot_token or SECRETS.discord_bot_token == "your_discord_bot_token_here":
|
|
errors.append("DISCORD_BOT_TOKEN not set or using placeholder value")
|
|
|
|
# Validate Cheshire Cat config
|
|
if CONFIG.cheshire_cat.enabled and not CONFIG.cheshire_cat.url:
|
|
errors.append("Cheshire Cat enabled but URL not configured")
|
|
|
|
return len(errors) == 0, errors
|
|
|
|
|
|
def print_config_summary():
|
|
"""Print a summary of current configuration (without secrets)"""
|
|
print("\n" + "="*60)
|
|
print("🎵 Miku Bot Configuration Summary")
|
|
print("="*60)
|
|
print(f"\n📊 Configuration loaded from: config.yaml")
|
|
print(f"🔐 Secrets loaded from: .env")
|
|
print(f"\n🤖 Models:")
|
|
print(f" - Text: {CONFIG.models.text}")
|
|
print(f" - Vision: {CONFIG.models.vision}")
|
|
print(f" - Evil: {CONFIG.models.evil}")
|
|
print(f" - Japanese: {CONFIG.models.japanese}")
|
|
print(f"\n🔗 Services:")
|
|
print(f" - Llama: {CONFIG.services.url}")
|
|
print(f" - Llama AMD: {CONFIG.services.amd_url}")
|
|
print(f" - Cheshire Cat: {CONFIG.cheshire_cat.url} (enabled: {CONFIG.cheshire_cat.enabled})")
|
|
print(f"\n⚙️ Settings:")
|
|
print(f" - Language Mode: {CONFIG.discord.language_mode}")
|
|
print(f" - Autonomous Debug: {CONFIG.autonomous.debug_mode}")
|
|
print(f" - Voice Debug: {CONFIG.voice.debug_mode}")
|
|
print(f" - Prefer AMD GPU: {CONFIG.gpu.prefer_amd}")
|
|
print(f"\n📝 Secrets: {'✅ Loaded' if SECRETS.discord_bot_token else '❌ Missing'}")
|
|
print("\n" + "="*60 + "\n")
|
|
|
|
|
|
# Auto-validate on import
|
|
is_valid, validation_errors = validate_config()
|
|
if not is_valid:
|
|
print("❌ Configuration Validation Failed:")
|
|
for error in validation_errors:
|
|
print(f" - {error}")
|
|
print("\nPlease check your .env file and try again.")
|
|
# Note: We don't exit here because the bot might be started in a different context
|
|
# The calling code should check validate_config() if needed
|