From 640667c9f95f2305a79564149bbf7d8725f8a687 Mon Sep 17 00:00:00 2001 From: Logan Date: Mon, 27 Apr 2026 00:50:01 -0400 Subject: [PATCH] Implement per-system AI flags --- drb-c2-core/app/routers/systems.py | 29 ++++++++++ drb-c2-core/app/routers/upload.py | 22 +++++-- drb-frontend/app/systems/page.tsx | 92 ++++++++++++++++++++++++++++++ drb-frontend/lib/c2api.ts | 7 +++ 4 files changed, 146 insertions(+), 4 deletions(-) diff --git a/drb-c2-core/app/routers/systems.py b/drb-c2-core/app/routers/systems.py index 23673f1..6783cfc 100644 --- a/drb-c2-core/app/routers/systems.py +++ b/drb-c2-core/app/routers/systems.py @@ -16,6 +16,11 @@ class TenCodesBody(BaseModel): ten_codes: Dict[str, str] +class AiFlagsBody(BaseModel): + stt_enabled: Optional[bool] = None + correlation_enabled: Optional[bool] = None + + @router.get("") async def list_systems(): return await fstore.collection_list("systems") @@ -54,6 +59,30 @@ async def delete_system(system_id: str): await fstore.doc_delete("systems", system_id) +# ── Per-system AI flag overrides ────────────────────────────────────────────── + +@router.put("/{system_id}/ai-flags") +async def update_system_ai_flags(system_id: str, body: AiFlagsBody): + """ + Set per-system AI flag overrides. Only fields included in the body are + written; omitted fields remain unchanged (or absent, meaning inherit global). + Pass null to clear an override and fall back to the global flag. + """ + existing = await fstore.doc_get("systems", system_id) + if not existing: + raise HTTPException(404, f"System '{system_id}' not found.") + + current: dict = existing.get("ai_flags") or {} + for field, value in body.model_dump(exclude_unset=False).items(): + if value is None: + current.pop(field, None) # clear override → inherit global + else: + current[field] = value + + await fstore.doc_update("systems", system_id, {"ai_flags": current}) + return {"ok": True, "ai_flags": current} + + # ── Ten-codes endpoints ──────────────────────────────────────────────────────── @router.get("/{system_id}/ten-codes") diff --git a/drb-c2-core/app/routers/upload.py b/drb-c2-core/app/routers/upload.py index bf82fbb..f43a75a 100644 --- a/drb-c2-core/app/routers/upload.py +++ b/drb-c2-core/app/routers/upload.py @@ -161,21 +161,34 @@ async def _run_intelligence_pipeline( flags = await get_flags() + # Resolve per-system overrides: system flag=False beats global flag=True, + # but global flag=False beats everything (master switch). + system_ai_flags: dict = {} + if system_id: + sys_doc = await fstore.doc_get("systems", system_id) + system_ai_flags = (sys_doc or {}).get("ai_flags") or {} + + def _flag(name: str) -> bool: + if not flags[name]: # global master off + return False + return system_ai_flags.get(name, True) # system override, default inherit + transcript: Optional[str] = None segments: list[dict] = [] # Step 1: Transcription if gcs_uri: - if flags["stt_enabled"]: + if _flag("stt_enabled"): transcript, segments = await transcription.transcribe_call( call_id, gcs_uri, talkgroup_name, system_id=system_id ) else: - logger.info(f"STT disabled — skipping transcription for call {call_id}") + scope = "globally" if not flags["stt_enabled"] else f"system {system_id}" + logger.info(f"STT disabled ({scope}) — skipping transcription for call {call_id}") # Step 2: Scene detection + intelligence extraction scenes: list[dict] = [] - if flags["correlation_enabled"]: + if _flag("correlation_enabled"): if transcript: scenes = await intelligence.extract_scenes( call_id, transcript, talkgroup_name, @@ -183,7 +196,8 @@ async def _run_intelligence_pipeline( node_id=node_id, ) else: - logger.info(f"Correlation disabled — skipping scene extraction and correlation for call {call_id}") + scope = "globally" if not flags["correlation_enabled"] else f"system {system_id}" + logger.info(f"Correlation disabled ({scope}) — skipping scene extraction and correlation for call {call_id}") # Step 3: Correlate each scene independently. # A single recording can produce multiple incidents on a busy channel. diff --git a/drb-frontend/app/systems/page.tsx b/drb-frontend/app/systems/page.tsx index 5a985a4..2d3ad74 100644 --- a/drb-frontend/app/systems/page.tsx +++ b/drb-frontend/app/systems/page.tsx @@ -433,6 +433,97 @@ function SystemForm({ ); } +// ── Per-system AI flags panel ───────────────────────────────────────────────── + +interface SystemAiFlags { + stt_enabled?: boolean; + correlation_enabled?: boolean; +} + +function AiFlagsPanel({ systemId, initial }: { systemId: string; initial: SystemAiFlags }) { + const [flags, setFlags] = useState(initial); + const [saving, setSaving] = useState(null); + const [open, setOpen] = useState(false); + + async function handleToggle(key: keyof SystemAiFlags, value: boolean) { + setSaving(key); + try { + const result = await c2api.setSystemAiFlags(systemId, { [key]: value }); + setFlags(result.ai_flags as SystemAiFlags); + } finally { + setSaving(null); + } + } + + async function handleClear(key: keyof SystemAiFlags) { + setSaving(key); + try { + const result = await c2api.setSystemAiFlags(systemId, { [key]: null }); + setFlags(result.ai_flags as SystemAiFlags); + } finally { + setSaving(null); + } + } + + const rows: { key: keyof SystemAiFlags; label: string }[] = [ + { key: "stt_enabled", label: "Speech-to-Text" }, + { key: "correlation_enabled", label: "Incident Correlation" }, + ]; + + return ( +
+ + + {open && ( +
+ {rows.map(({ key, label }) => { + const override = flags[key]; + const isSet = override !== undefined; + const isOn = override !== false; + return ( +
+ + {label} + {isSet ? ( + + ) : ( + inherits global + )} +
+ ); + })} +

Overrides apply on top of global AI flags. "reset" restores global default.

+
+ )} +
+ ); +} + + // ── Vocabulary panel ────────────────────────────────────────────────────────── function VocabularyPanel({ systemId }: { systemId: string }) { @@ -692,6 +783,7 @@ export default function SystemsPage() { Delete + ); diff --git a/drb-frontend/lib/c2api.ts b/drb-frontend/lib/c2api.ts index cddf9a0..31c3128 100644 --- a/drb-frontend/lib/c2api.ts +++ b/drb-frontend/lib/c2api.ts @@ -121,4 +121,11 @@ export const c2api = { request>("/admin/features"), setFeatureFlags: (flags: Record) => request>("/admin/features", { method: "PUT", body: JSON.stringify(flags) }), + + // Per-system AI flag overrides + setSystemAiFlags: (systemId: string, flags: { stt_enabled?: boolean | null; correlation_enabled?: boolean | null }) => + request<{ ok: boolean; ai_flags: Record }>(`/systems/${systemId}/ai-flags`, { + method: "PUT", + body: JSON.stringify(flags), + }), };