2026-06-19 11:28:04 +03:00
|
|
|
"""Admin settings service (DB-backed)."""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
|
|
|
|
|
|
from sqlalchemy import select
|
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
|
|
|
|
from app.models import Setting
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Settings that can be edited by admin via the admin panel
|
|
|
|
|
EDITABLE_SETTING_KEYS = {
|
|
|
|
|
"llm.base_url": str,
|
|
|
|
|
"llm.api_key": str,
|
|
|
|
|
"llm.model": str,
|
|
|
|
|
"llm.temperature": float,
|
|
|
|
|
"llm.step_temperature": float,
|
|
|
|
|
"llm.summary_temperature": float,
|
|
|
|
|
"llm.max_tokens": int,
|
|
|
|
|
"llm.request_timeout": int,
|
|
|
|
|
"llm.streaming": bool,
|
|
|
|
|
"context.recent_messages": int,
|
|
|
|
|
"context.compress_threshold": int,
|
|
|
|
|
"context.summary_messages": int,
|
|
|
|
|
"context.max_tokens_total": int,
|
|
|
|
|
"triggers.enabled": bool,
|
2026-06-19 16:31:45 +03:00
|
|
|
# Note: triggers.check_interval was removed — triggers now fire in-process
|
|
|
|
|
# when in-game time changes, not via a polling worker.
|
2026-06-19 11:28:04 +03:00
|
|
|
# Embeddings / RAG
|
|
|
|
|
"embedding.provider": str, # "hash" | "openai"
|
|
|
|
|
"embedding.base_url": str, # OpenAI-compatible base URL (e.g. http://localhost:1234/v1)
|
|
|
|
|
"embedding.api_key": str, # API key (may be empty for local servers)
|
|
|
|
|
"embedding.model": str, # e.g. text-embedding-3-small, bge-m3, nomic-embed-text
|
|
|
|
|
"embedding.dim": int, # vector dimension; 0 = auto-probe from endpoint
|
|
|
|
|
"embedding.request_timeout": int, # request timeout, seconds
|
2026-06-19 19:14:27 +03:00
|
|
|
# UI customization (logo URL/path shown in navbar + home page + favicon)
|
|
|
|
|
"ui.logo_url": str, # e.g. "/logo.png", "https://.../logo.png", or "data:image/png;base64,..."
|
2026-06-19 11:28:04 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def get_all_settings(db: AsyncSession) -> Dict[str, Any]:
|
|
|
|
|
result = await db.execute(select(Setting))
|
|
|
|
|
return {row.key: row.value for row in result.scalars().all()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def get_setting(db: AsyncSession, key: str, default: Any = None) -> Any:
|
|
|
|
|
result = await db.execute(select(Setting).where(Setting.key == key))
|
|
|
|
|
row = result.scalars().first()
|
|
|
|
|
return row.value if row else default
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def update_settings(db: AsyncSession, updates: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
|
|
for key, value in updates.items():
|
|
|
|
|
if key not in EDITABLE_SETTING_KEYS:
|
|
|
|
|
continue
|
|
|
|
|
expected = EDITABLE_SETTING_KEYS[key]
|
|
|
|
|
try:
|
|
|
|
|
if expected is bool:
|
|
|
|
|
value = bool(value)
|
|
|
|
|
elif expected is int:
|
|
|
|
|
value = int(value)
|
|
|
|
|
elif expected is float:
|
|
|
|
|
value = float(value)
|
|
|
|
|
else:
|
|
|
|
|
value = str(value)
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
continue
|
|
|
|
|
result = await db.execute(select(Setting).where(Setting.key == key))
|
|
|
|
|
row = result.scalars().first()
|
|
|
|
|
if row is None:
|
|
|
|
|
db.add(Setting(key=key, value=value))
|
|
|
|
|
else:
|
|
|
|
|
row.value = value
|
|
|
|
|
await db.commit()
|
|
|
|
|
return await get_all_settings(db)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def cast_setting(key: str, value: Any) -> Any:
|
|
|
|
|
"""Cast raw DB value to the expected type for use."""
|
|
|
|
|
if key not in EDITABLE_SETTING_KEYS:
|
|
|
|
|
return value
|
|
|
|
|
expected = EDITABLE_SETTING_KEYS[key]
|
|
|
|
|
try:
|
|
|
|
|
if expected is bool:
|
|
|
|
|
if isinstance(value, str):
|
|
|
|
|
return value.lower() in ("1", "true", "yes", "on")
|
|
|
|
|
return bool(value)
|
|
|
|
|
if expected is int:
|
|
|
|
|
return int(value)
|
|
|
|
|
if expected is float:
|
|
|
|
|
return float(value)
|
|
|
|
|
return str(value)
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
return value
|