Implement per-system AI flags
This commit is contained in:
@@ -16,6 +16,11 @@ class TenCodesBody(BaseModel):
|
|||||||
ten_codes: Dict[str, str]
|
ten_codes: Dict[str, str]
|
||||||
|
|
||||||
|
|
||||||
|
class AiFlagsBody(BaseModel):
|
||||||
|
stt_enabled: Optional[bool] = None
|
||||||
|
correlation_enabled: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
@router.get("")
|
@router.get("")
|
||||||
async def list_systems():
|
async def list_systems():
|
||||||
return await fstore.collection_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)
|
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 ────────────────────────────────────────────────────────
|
# ── Ten-codes endpoints ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@router.get("/{system_id}/ten-codes")
|
@router.get("/{system_id}/ten-codes")
|
||||||
|
|||||||
@@ -161,21 +161,34 @@ async def _run_intelligence_pipeline(
|
|||||||
|
|
||||||
flags = await get_flags()
|
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
|
transcript: Optional[str] = None
|
||||||
segments: list[dict] = []
|
segments: list[dict] = []
|
||||||
|
|
||||||
# Step 1: Transcription
|
# Step 1: Transcription
|
||||||
if gcs_uri:
|
if gcs_uri:
|
||||||
if flags["stt_enabled"]:
|
if _flag("stt_enabled"):
|
||||||
transcript, segments = await transcription.transcribe_call(
|
transcript, segments = await transcription.transcribe_call(
|
||||||
call_id, gcs_uri, talkgroup_name, system_id=system_id
|
call_id, gcs_uri, talkgroup_name, system_id=system_id
|
||||||
)
|
)
|
||||||
else:
|
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
|
# Step 2: Scene detection + intelligence extraction
|
||||||
scenes: list[dict] = []
|
scenes: list[dict] = []
|
||||||
if flags["correlation_enabled"]:
|
if _flag("correlation_enabled"):
|
||||||
if transcript:
|
if transcript:
|
||||||
scenes = await intelligence.extract_scenes(
|
scenes = await intelligence.extract_scenes(
|
||||||
call_id, transcript, talkgroup_name,
|
call_id, transcript, talkgroup_name,
|
||||||
@@ -183,7 +196,8 @@ async def _run_intelligence_pipeline(
|
|||||||
node_id=node_id,
|
node_id=node_id,
|
||||||
)
|
)
|
||||||
else:
|
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.
|
# Step 3: Correlate each scene independently.
|
||||||
# A single recording can produce multiple incidents on a busy channel.
|
# 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 ──────────────────────────────────────────────────────────
|
// ── Vocabulary panel ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function VocabularyPanel({ systemId }: { systemId: string }) {
|
function VocabularyPanel({ systemId }: { systemId: string }) {
|
||||||
@@ -692,6 +783,7 @@ export default function SystemsPage() {
|
|||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<AiFlagsPanel systemId={s.system_id} initial={(s as unknown as { ai_flags?: SystemAiFlags }).ai_flags ?? {}} />
|
||||||
<VocabularyPanel systemId={s.system_id} />
|
<VocabularyPanel systemId={s.system_id} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -121,4 +121,11 @@ export const c2api = {
|
|||||||
request<Record<string, boolean>>("/admin/features"),
|
request<Record<string, boolean>>("/admin/features"),
|
||||||
setFeatureFlags: (flags: Record<string, boolean>) =>
|
setFeatureFlags: (flags: Record<string, boolean>) =>
|
||||||
request<Record<string, boolean>>("/admin/features", { method: "PUT", body: JSON.stringify(flags) }),
|
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