""" 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