115 lines
3.7 KiB
Python
115 lines
3.7 KiB
Python
"""
|
|
Background incident summary loop.
|
|
|
|
Runs every SUMMARY_INTERVAL_MINUTES. Finds all active incidents with
|
|
summary_stale=True, fetches all their call transcripts, and calls Gemini
|
|
once per incident to produce a concise factual summary.
|
|
|
|
By batching this way: Gemini is never called per-call — only periodically
|
|
and only for incidents that have actually changed since the last run.
|
|
"""
|
|
import asyncio
|
|
from datetime import datetime, timezone
|
|
from typing import Optional
|
|
from app.internal.logger import logger
|
|
from app.internal import firestore as fstore
|
|
from app.config import settings
|
|
|
|
|
|
async def summarizer_loop() -> None:
|
|
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()
|
|
except Exception as e:
|
|
logger.error(f"Summarizer pass failed: {e}")
|
|
|
|
|
|
async def _run_summary_pass() -> None:
|
|
stale = await fstore.collection_list("incidents", status="active", summary_stale=True)
|
|
if not stale:
|
|
return
|
|
|
|
logger.info(f"Summarizer: processing {len(stale)} stale incident(s)")
|
|
for inc in stale:
|
|
await _summarize_incident(inc)
|
|
|
|
|
|
async def _summarize_incident(inc: dict) -> None:
|
|
incident_id = inc.get("incident_id")
|
|
if not incident_id:
|
|
return
|
|
|
|
call_ids: list[str] = inc.get("call_ids", [])
|
|
if not call_ids:
|
|
return
|
|
|
|
# Fetch transcripts for all calls in this incident
|
|
transcripts: list[str] = []
|
|
for cid in call_ids:
|
|
doc = await fstore.doc_get("calls", cid)
|
|
if doc and doc.get("transcript"):
|
|
transcripts.append(doc["transcript"])
|
|
|
|
if not transcripts:
|
|
# No transcripts yet — clear stale flag and wait for next pass
|
|
await fstore.doc_set("incidents", incident_id, {"summary_stale": False})
|
|
return
|
|
|
|
summary = await asyncio.to_thread(_sync_summarize, inc, transcripts)
|
|
|
|
now = datetime.now(timezone.utc).isoformat()
|
|
updates: dict = {
|
|
"summary_stale": False,
|
|
"summary_last_run": now,
|
|
}
|
|
if summary:
|
|
updates["summary"] = summary
|
|
logger.info(f"Summarizer: updated summary for incident {incident_id}")
|
|
else:
|
|
logger.warning(f"Summarizer: Gemini returned nothing for incident {incident_id}")
|
|
|
|
await fstore.doc_set("incidents", incident_id, updates)
|
|
|
|
|
|
def _sync_summarize(inc: dict, transcripts: list[str]) -> Optional[str]:
|
|
from app.config import settings
|
|
import google.generativeai as genai
|
|
|
|
if not settings.gemini_api_key:
|
|
return None
|
|
|
|
genai.configure(api_key=settings.gemini_api_key)
|
|
model = genai.GenerativeModel("gemini-1.5-flash")
|
|
|
|
inc_type = inc.get("type", "unknown")
|
|
location = inc.get("location") or "unknown location"
|
|
tg_ids = ", ".join(inc.get("talkgroup_ids", [])) or "unknown"
|
|
numbered = "\n".join(f"{i+1}. {t}" for i, t in enumerate(transcripts))
|
|
|
|
prompt = f"""You are analyzing P25 public safety radio communications for a single active incident.
|
|
|
|
Incident type: {inc_type}
|
|
Location: {location}
|
|
Talkgroup(s): {tg_ids}
|
|
|
|
Transcripts ({len(transcripts)} calls, chronological):
|
|
{numbered}
|
|
|
|
Write a concise factual summary of this incident in 2-4 sentences. Include:
|
|
- What happened
|
|
- Location (most specific mentioned)
|
|
- Units or resources involved if mentioned
|
|
- Current status if determinable
|
|
|
|
Be factual. Do not speculate beyond what the transcripts say. Do not use bullet points."""
|
|
|
|
try:
|
|
response = model.generate_content(prompt)
|
|
return response.text.strip() or None
|
|
except Exception as e:
|
|
logger.warning(f"Gemini summary failed: {e}")
|
|
return None
|