Implement per-system AI flags
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<SystemAiFlags>(initial);
|
||||
const [saving, setSaving] = useState<string | null>(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 (
|
||||
<div className="mt-3 border-t border-gray-800 pt-3">
|
||||
<button
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="text-xs text-gray-500 hover:text-gray-300 font-mono transition-colors flex items-center gap-1"
|
||||
>
|
||||
<span>{open ? "▲" : "▼"}</span>
|
||||
<span>AI Flags</span>
|
||||
{(flags.stt_enabled === false || flags.correlation_enabled === false) && (
|
||||
<span className="ml-1.5 text-yellow-600 font-bold">!</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="mt-3 space-y-2 font-mono text-xs">
|
||||
{rows.map(({ key, label }) => {
|
||||
const override = flags[key];
|
||||
const isSet = override !== undefined;
|
||||
const isOn = override !== false;
|
||||
return (
|
||||
<div key={key} className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => handleToggle(key, !isOn)}
|
||||
disabled={saving === key}
|
||||
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors disabled:opacity-50 ${
|
||||
isOn ? "bg-indigo-600" : "bg-gray-700"
|
||||
}`}
|
||||
>
|
||||
<span className={`inline-block h-3.5 w-3.5 rounded-full bg-white shadow transition-transform ${isOn ? "translate-x-4" : "translate-x-0.5"}`} />
|
||||
</button>
|
||||
<span className="text-gray-300 flex-1">{label}</span>
|
||||
{isSet ? (
|
||||
<button
|
||||
onClick={() => handleClear(key)}
|
||||
disabled={saving === key}
|
||||
className="text-gray-600 hover:text-gray-400 transition-colors"
|
||||
title="Clear override (inherit global)"
|
||||
>
|
||||
reset
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-gray-700">inherits global</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<p className="text-gray-700 pt-1">Overrides apply on top of global AI flags. "reset" restores global default.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// ── Vocabulary panel ──────────────────────────────────────────────────────────
|
||||
|
||||
function VocabularyPanel({ systemId }: { systemId: string }) {
|
||||
@@ -692,6 +783,7 @@ export default function SystemsPage() {
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
<AiFlagsPanel systemId={s.system_id} initial={(s as unknown as { ai_flags?: SystemAiFlags }).ai_flags ?? {}} />
|
||||
<VocabularyPanel systemId={s.system_id} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -121,4 +121,11 @@ export const c2api = {
|
||||
request<Record<string, boolean>>("/admin/features"),
|
||||
setFeatureFlags: (flags: Record<string, boolean>) =>
|
||||
request<Record<string, boolean>>("/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<string, boolean> }>(`/systems/${systemId}/ai-flags`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(flags),
|
||||
}),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user