Implement comprehensive config system and clean up codebase
Major changes: - Add Pydantic-based configuration system (bot/config.py, bot/config_manager.py) - Add config.yaml with all service URLs, models, and feature flags - Fix config.yaml path resolution in Docker (check /app/config.yaml first) - Remove Fish Audio API integration (tested feature that didn't work) - Remove hardcoded ERROR_WEBHOOK_URL, import from config instead - Add missing Pydantic models (LogConfigUpdateRequest, LogFilterUpdateRequest) - Enable Cheshire Cat memory system by default (USE_CHESHIRE_CAT=true) - Add .env.example template with all required environment variables - Add setup.sh script for user-friendly initialization - Update docker-compose.yml with proper env file mounting - Update .gitignore for config files and temporary files Config system features: - Static configuration from config.yaml - Runtime overrides from config_runtime.yaml - Environment variables for secrets (.env) - Web UI integration via config_manager - Graceful fallback to defaults Secrets handling: - Move ERROR_WEBHOOK_URL from hardcoded to .env - Add .env.example with all placeholder values - Document all required secrets - Fish API key and voice ID removed from .env Documentation: - CONFIG_README.md - Configuration system guide - CONFIG_SYSTEM_COMPLETE.md - Implementation summary - FISH_API_REMOVAL_COMPLETE.md - Removal record - SECRETS_CONFIGURED.md - Secrets setup record - BOT_STARTUP_FIX.md - Pydantic model fixes - MIGRATION_CHECKLIST.md - Setup checklist - WEB_UI_INTEGRATION_COMPLETE.md - Web UI config guide - Updated readmes/README.md with new features
This commit is contained in:
240
bot/api.py
240
bot/api.py
@@ -170,6 +170,17 @@ class ServerConfigRequest(BaseModel):
|
||||
class EvilMoodSetRequest(BaseModel):
|
||||
mood: str
|
||||
|
||||
class LogConfigUpdateRequest(BaseModel):
|
||||
component: Optional[str] = None
|
||||
enabled: Optional[bool] = None
|
||||
enabled_levels: Optional[List[str]] = None
|
||||
|
||||
class LogFilterUpdateRequest(BaseModel):
|
||||
exclude_paths: Optional[List[str]] = None
|
||||
exclude_status: Optional[List[int]] = None
|
||||
include_slow_requests: Optional[bool] = True
|
||||
slow_threshold_ms: Optional[int] = 1000
|
||||
|
||||
# ========== Routes ==========
|
||||
@app.get("/")
|
||||
def read_index():
|
||||
@@ -206,6 +217,13 @@ async def set_mood_endpoint(data: MoodSetRequest):
|
||||
from utils.moods import load_mood_description
|
||||
globals.DM_MOOD_DESCRIPTION = load_mood_description(data.mood)
|
||||
|
||||
# Persist to config manager
|
||||
try:
|
||||
from config_manager import config_manager
|
||||
config_manager.set("runtime.mood.dm_mood", data.mood, persist=True)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to persist mood to config: {e}")
|
||||
|
||||
return {"status": "ok", "new_mood": data.mood}
|
||||
|
||||
@app.post("/mood/reset")
|
||||
@@ -215,6 +233,13 @@ async def reset_mood_endpoint():
|
||||
from utils.moods import load_mood_description
|
||||
globals.DM_MOOD_DESCRIPTION = load_mood_description("neutral")
|
||||
|
||||
# Persist to config manager
|
||||
try:
|
||||
from config_manager import config_manager
|
||||
config_manager.set("runtime.mood.dm_mood", "neutral", persist=True)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to persist mood reset to config: {e}")
|
||||
|
||||
return {"status": "ok", "new_mood": "neutral"}
|
||||
|
||||
@app.post("/mood/calm")
|
||||
@@ -224,6 +249,13 @@ def calm_miku_endpoint():
|
||||
from utils.moods import load_mood_description
|
||||
globals.DM_MOOD_DESCRIPTION = load_mood_description("neutral")
|
||||
|
||||
# Persist to config manager
|
||||
try:
|
||||
from config_manager import config_manager
|
||||
config_manager.set("runtime.mood.dm_mood", "neutral", persist=True)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to persist mood calm to config: {e}")
|
||||
|
||||
return {"status": "ok", "message": "Miku has been calmed down"}
|
||||
|
||||
# ========== Language Mode Management ==========
|
||||
@@ -250,6 +282,14 @@ def toggle_language_mode():
|
||||
model_used = globals.TEXT_MODEL
|
||||
logger.info("Switched to English mode (using default model)")
|
||||
|
||||
# Persist via config manager
|
||||
try:
|
||||
from config_manager import config_manager
|
||||
config_manager.set("discord.language_mode", new_mode, persist=True)
|
||||
logger.info(f"💾 Language mode persisted to config_runtime.yaml")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to persist language mode: {e}")
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"language_mode": new_mode,
|
||||
@@ -402,6 +442,14 @@ def enable_bipolar_mode():
|
||||
return {"status": "ok", "message": "Bipolar mode is already enabled", "bipolar_mode": True}
|
||||
|
||||
_enable()
|
||||
|
||||
# Persist to config manager
|
||||
try:
|
||||
from config_manager import config_manager
|
||||
config_manager.set("runtime.bipolar_mode.enabled", True, persist=True)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to persist bipolar mode enable to config: {e}")
|
||||
|
||||
return {"status": "ok", "message": "Bipolar mode enabled", "bipolar_mode": True}
|
||||
|
||||
@app.post("/bipolar-mode/disable")
|
||||
@@ -414,6 +462,13 @@ def disable_bipolar_mode():
|
||||
|
||||
_disable()
|
||||
|
||||
# Persist to config manager
|
||||
try:
|
||||
from config_manager import config_manager
|
||||
config_manager.set("runtime.bipolar_mode.enabled", False, persist=True)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to persist bipolar mode disable to config: {e}")
|
||||
|
||||
# Optionally cleanup webhooks in background
|
||||
if globals.client and globals.client.loop and globals.client.loop.is_running():
|
||||
globals.client.loop.create_task(cleanup_webhooks(globals.client))
|
||||
@@ -644,18 +699,15 @@ async def select_gpu(request: Request):
|
||||
if gpu not in ["nvidia", "amd"]:
|
||||
return {"status": "error", "message": "Invalid GPU selection. Must be 'nvidia' or 'amd'"}
|
||||
|
||||
gpu_state_file = os.path.join(os.path.dirname(__file__), "memory", "gpu_state.json")
|
||||
try:
|
||||
from datetime import datetime
|
||||
state = {
|
||||
"current_gpu": gpu,
|
||||
"last_updated": datetime.now().isoformat()
|
||||
}
|
||||
with open(gpu_state_file, "w") as f:
|
||||
json.dump(state, f, indent=2)
|
||||
from config_manager import config_manager
|
||||
success = config_manager.set_gpu(gpu)
|
||||
|
||||
logger.info(f"GPU Selection: Switched to {gpu.upper()} GPU")
|
||||
return {"status": "ok", "message": f"Switched to {gpu.upper()} GPU", "gpu": gpu}
|
||||
if success:
|
||||
logger.info(f"GPU Selection: Switched to {gpu.upper()} GPU")
|
||||
return {"status": "ok", "message": f"Switched to {gpu.upper()} GPU", "gpu": gpu}
|
||||
else:
|
||||
return {"status": "error", "message": "Failed to save GPU state"}
|
||||
except Exception as e:
|
||||
logger.error(f"GPU Selection Error: {e}")
|
||||
return {"status": "error", "message": str(e)}
|
||||
@@ -2415,18 +2467,164 @@ Be detailed but conversational. React to what you see with Miku's cheerful, play
|
||||
}
|
||||
)
|
||||
|
||||
# ========== Log Management API ==========
|
||||
class LogConfigUpdateRequest(BaseModel):
|
||||
component: Optional[str] = None
|
||||
enabled: Optional[bool] = None
|
||||
enabled_levels: Optional[List[str]] = None
|
||||
|
||||
class LogFilterUpdateRequest(BaseModel):
|
||||
exclude_paths: Optional[List[str]] = None
|
||||
exclude_status: Optional[List[int]] = None
|
||||
include_slow_requests: Optional[bool] = None
|
||||
slow_threshold_ms: Optional[int] = None
|
||||
# ========== Configuration Management (New Unified System) ==========
|
||||
|
||||
@app.get("/config")
|
||||
async def get_full_config():
|
||||
"""
|
||||
Get full configuration including static, runtime, and state.
|
||||
Useful for debugging and config display in UI.
|
||||
"""
|
||||
try:
|
||||
from config_manager import config_manager
|
||||
full_config = config_manager.get_full_config()
|
||||
return {
|
||||
"success": True,
|
||||
"config": full_config
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get config: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
@app.get("/config/static")
|
||||
async def get_static_config():
|
||||
"""
|
||||
Get static configuration from config.yaml.
|
||||
These are default values that can be overridden at runtime.
|
||||
"""
|
||||
try:
|
||||
from config_manager import config_manager
|
||||
return {
|
||||
"success": True,
|
||||
"config": config_manager.static_config
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get static config: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
@app.get("/config/runtime")
|
||||
async def get_runtime_config():
|
||||
"""
|
||||
Get runtime configuration overrides.
|
||||
These are values changed via Web UI that override config.yaml.
|
||||
"""
|
||||
try:
|
||||
from config_manager import config_manager
|
||||
return {
|
||||
"success": True,
|
||||
"config": config_manager.runtime_config,
|
||||
"path": str(config_manager.runtime_config_path)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get runtime config: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
@app.post("/config/set")
|
||||
async def set_config_value(request: Request):
|
||||
"""
|
||||
Set a configuration value with optional persistence.
|
||||
|
||||
Body: {
|
||||
"key_path": "discord.language_mode", // Dot-separated path
|
||||
"value": "japanese",
|
||||
"persist": true // Save to config_runtime.yaml
|
||||
}
|
||||
"""
|
||||
try:
|
||||
data = await request.json()
|
||||
key_path = data.get("key_path")
|
||||
value = data.get("value")
|
||||
persist = data.get("persist", True)
|
||||
|
||||
if not key_path:
|
||||
return {"success": False, "error": "key_path is required"}
|
||||
|
||||
from config_manager import config_manager
|
||||
config_manager.set(key_path, value, persist=persist)
|
||||
|
||||
# Update globals if needed
|
||||
if key_path == "discord.language_mode":
|
||||
globals.LANGUAGE_MODE = value
|
||||
elif key_path == "autonomous.debug_mode":
|
||||
globals.AUTONOMOUS_DEBUG = value
|
||||
elif key_path == "voice.debug_mode":
|
||||
globals.VOICE_DEBUG_MODE = value
|
||||
elif key_path == "gpu.prefer_amd":
|
||||
globals.PREFER_AMD_GPU = value
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Set {key_path} = {value}",
|
||||
"persisted": persist
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to set config: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
@app.post("/config/reset")
|
||||
async def reset_config(request: Request):
|
||||
"""
|
||||
Reset configuration to defaults.
|
||||
|
||||
Body: {
|
||||
"key_path": "discord.language_mode", // Optional: reset specific key
|
||||
"persist": true // Remove from config_runtime.yaml
|
||||
}
|
||||
|
||||
If key_path is omitted, resets all runtime config to defaults.
|
||||
"""
|
||||
try:
|
||||
data = await request.json()
|
||||
key_path = data.get("key_path")
|
||||
persist = data.get("persist", True)
|
||||
|
||||
from config_manager import config_manager
|
||||
config_manager.reset_to_defaults(key_path)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Reset {key_path or 'all config'} to defaults"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to reset config: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
@app.post("/config/validate")
|
||||
async def validate_config_endpoint():
|
||||
"""
|
||||
Validate current configuration.
|
||||
Returns list of errors if validation fails.
|
||||
"""
|
||||
try:
|
||||
from config_manager import config_manager
|
||||
is_valid, errors = config_manager.validate_config()
|
||||
|
||||
return {
|
||||
"success": is_valid,
|
||||
"is_valid": is_valid,
|
||||
"errors": errors
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to validate config: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
@app.get("/config/state")
|
||||
async def get_config_state():
|
||||
"""
|
||||
Get runtime state (not persisted config).
|
||||
These are transient values like current mood, evil mode, etc.
|
||||
"""
|
||||
try:
|
||||
from config_manager import config_manager
|
||||
return {
|
||||
"success": True,
|
||||
"state": config_manager.runtime_state
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get config state: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
# ========== Logging Configuration (Existing System) ==========
|
||||
@app.get("/api/log/config")
|
||||
async def get_log_config():
|
||||
"""Get current logging configuration."""
|
||||
|
||||
Reference in New Issue
Block a user