Files
miku-discord/bot/config.py
koko210Serve 0831f721e1 cleanup: remove dead backward-compat globals from config.py
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.
2026-04-08 14:40:16 +03:00

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