63 lines
1.9 KiB
Python
63 lines
1.9 KiB
Python
"""
|
|
Global AI feature flags stored in Firestore at config/ai_features.
|
|
|
|
Defaults to all-on when the document does not exist yet. Uses a short
|
|
in-memory TTL cache so flag reads don't add a Firestore round-trip to every
|
|
call upload.
|
|
"""
|
|
import time
|
|
from typing import Any
|
|
from app.internal.logger import logger
|
|
from app.internal import firestore as fstore
|
|
|
|
_COLLECTION = "config"
|
|
_DOC_ID = "ai_features"
|
|
_TTL = 30.0 # seconds before re-reading from Firestore
|
|
|
|
_DEFAULTS: dict[str, bool] = {
|
|
"stt_enabled": True,
|
|
"correlation_enabled": True,
|
|
"summaries_enabled": True,
|
|
"vocabulary_learning_enabled": True,
|
|
}
|
|
|
|
_cache: dict[str, Any] = {}
|
|
_cache_ts: float = 0.0
|
|
|
|
|
|
async def get_flags() -> dict[str, bool]:
|
|
"""Return the current feature flags, using the TTL cache when fresh."""
|
|
global _cache, _cache_ts
|
|
|
|
now = time.monotonic()
|
|
if _cache and (now - _cache_ts) < _TTL:
|
|
return dict(_cache)
|
|
|
|
try:
|
|
doc = await fstore.doc_get(_COLLECTION, _DOC_ID)
|
|
if doc:
|
|
merged = {**_DEFAULTS, **{k: bool(v) for k, v in doc.items() if k in _DEFAULTS}}
|
|
else:
|
|
merged = dict(_DEFAULTS)
|
|
except Exception as e:
|
|
logger.warning(f"Feature flags: could not read from Firestore ({e}), using defaults")
|
|
merged = dict(_DEFAULTS)
|
|
|
|
_cache = merged
|
|
_cache_ts = now
|
|
return dict(_cache)
|
|
|
|
|
|
async def set_flags(updates: dict[str, bool]) -> dict[str, bool]:
|
|
"""Write flag updates to Firestore and invalidate the cache."""
|
|
global _cache, _cache_ts
|
|
|
|
clean = {k: bool(v) for k, v in updates.items() if k in _DEFAULTS}
|
|
if not clean:
|
|
raise ValueError(f"No recognised flag keys in update: {list(updates)}")
|
|
|
|
await fstore.doc_set(_COLLECTION, _DOC_ID, clean)
|
|
_cache_ts = 0.0 # force re-read on next get_flags()
|
|
logger.info(f"Feature flags updated: {clean}")
|
|
return await get_flags()
|