Update intelligence
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user