Implement Admin UI to disable AI components
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
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()
|
||||
@@ -16,13 +16,18 @@ from app.config import settings
|
||||
|
||||
|
||||
async def summarizer_loop() -> None:
|
||||
from app.internal.feature_flags import get_flags
|
||||
interval = settings.summary_interval_minutes * 60
|
||||
logger.info(f"Summarizer started — interval: {settings.summary_interval_minutes}m")
|
||||
while True:
|
||||
await asyncio.sleep(interval)
|
||||
try:
|
||||
await _run_summary_pass()
|
||||
await _resolve_stale_incidents()
|
||||
flags = await get_flags()
|
||||
if flags["summaries_enabled"]:
|
||||
await _run_summary_pass()
|
||||
await _resolve_stale_incidents()
|
||||
else:
|
||||
logger.info("Summaries disabled — skipping summary pass and stale incident sweep")
|
||||
except Exception as e:
|
||||
logger.error(f"Summarizer pass failed: {e}")
|
||||
|
||||
|
||||
@@ -243,6 +243,7 @@ def build_gpt_vocab_block(vocabulary: list[str]) -> str:
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
async def vocabulary_induction_loop() -> None:
|
||||
from app.internal.feature_flags import get_flags
|
||||
interval = settings.vocabulary_induction_interval_hours * 3600
|
||||
logger.info(
|
||||
f"Vocabulary induction loop started — "
|
||||
@@ -252,7 +253,11 @@ async def vocabulary_induction_loop() -> None:
|
||||
while True:
|
||||
await asyncio.sleep(interval)
|
||||
try:
|
||||
await _run_induction_pass()
|
||||
flags = await get_flags()
|
||||
if flags["vocabulary_learning_enabled"]:
|
||||
await _run_induction_pass()
|
||||
else:
|
||||
logger.info("Vocabulary learning disabled — skipping induction pass")
|
||||
except Exception as e:
|
||||
logger.error(f"Vocabulary induction pass failed: {e}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user