Correlator prompt: incident age/size + new-event rule (#189) #191
@@ -31,18 +31,24 @@ from app.config import settings
|
||||
# Prompt helpers
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _fmt_idle(inc: dict, now: datetime) -> str:
|
||||
def _fmt_since(raw, now: datetime) -> str:
|
||||
try:
|
||||
raw = inc.get("updated_at") or inc.get("started_at") or ""
|
||||
dt = datetime.fromisoformat(str(raw).replace("Z", "+00:00"))
|
||||
dt = datetime.fromisoformat(str(raw or "").replace("Z", "+00:00"))
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
minutes = int((now - dt).total_seconds() / 60)
|
||||
return f"{minutes}min ago" if minutes < 60 else f"{minutes // 60}h{minutes % 60:02d}m ago"
|
||||
# Clamped: the recorrelation sweep back-dates `now` (see
|
||||
# _idle_gate_minutes), which would otherwise print "-12min".
|
||||
minutes = max(int((now - dt).total_seconds() / 60), 0)
|
||||
return f"{minutes}min" if minutes < 60 else f"{minutes // 60}h{minutes % 60:02d}m"
|
||||
except Exception:
|
||||
return "?"
|
||||
|
||||
|
||||
def _fmt_idle(inc: dict, now: datetime) -> str:
|
||||
since = _fmt_since(inc.get("updated_at") or inc.get("started_at"), now)
|
||||
return since if since == "?" else f"{since} ago"
|
||||
|
||||
|
||||
def _inc_summary(inc: dict, now: datetime) -> str:
|
||||
# server-26#115: the model was given no title and no talkgroup, so it
|
||||
# could not tell that "car alarms, Mohegan Park Ave" and "car alarms,
|
||||
@@ -65,6 +71,19 @@ def _inc_summary(inc: dict, now: datetime) -> str:
|
||||
if tags:
|
||||
parts.append(f"tags:[{', '.join(tags[:4])}]")
|
||||
parts.append(f"idle:{_fmt_idle(inc, now)}")
|
||||
# server-26#189: idle alone made a 2.5h-old job look as fresh as a
|
||||
# 5-minute one — every link resets idle, so a busy channel's oldest
|
||||
# incident always reads "2min ago" and keeps winning. Age and size let
|
||||
# the model see it has become a catch-all.
|
||||
if inc.get("started_at"):
|
||||
from app.internal.incident_correlator import _incident_span_minutes
|
||||
span = int(_incident_span_minutes(inc, now))
|
||||
parts.append(f"open:{span}min" if span < 60 else f"open:{span // 60}h{span % 60:02d}m")
|
||||
n_calls = inc.get("substantive_call_count")
|
||||
if n_calls is None:
|
||||
n_calls = len(inc.get("call_ids") or [])
|
||||
if n_calls:
|
||||
parts.append(f"calls:{n_calls}")
|
||||
return " | ".join(parts)
|
||||
|
||||
|
||||
@@ -90,6 +109,8 @@ def _call_block(ctx: dict) -> str:
|
||||
lines.append(f"Units: {ctx['call_units']}")
|
||||
if ctx["call_vehicles"]:
|
||||
lines.append(f"Vehicles: {ctx['call_vehicles']}")
|
||||
if ctx.get("reassignment"):
|
||||
lines.append("Reassignment: yes — a unit is being sent to a NEW job")
|
||||
if ctx["talkgroup_name"] or ctx.get("talkgroup_id") is not None:
|
||||
# Both the name and the id — _inc_summary emits numeric tg ids, so the
|
||||
# id is what makes the "same talkgroup" rule in _RULES evaluable
|
||||
@@ -116,16 +137,23 @@ def _prompt_incidents(recent: list[dict]) -> list[dict]:
|
||||
_SCHEMA = '{"action": "link" | "new" | "orphan", "incident_id": "<id_string or null>", "reasoning": "<one sentence>"}'
|
||||
|
||||
_RULES = """
|
||||
Rules (this system OVER-SPLITS — a real incident routinely gets shattered into
|
||||
5-10 duplicates. A wrong link is cheap; a duplicate incident is the failure
|
||||
mode. Bias accordingly.):
|
||||
Rules (this system usually OVER-SPLITS — a real incident often gets shattered
|
||||
into 5-10 duplicates, so lean toward "link" for follow-ups. The exception is a
|
||||
call that reports a new event, below.):
|
||||
- Prefer "link" when the call plausibly continues a recent incident ON THE SAME
|
||||
TALKGROUP: same or overlapping units, the same or an adjacent location (treat
|
||||
"Ave"/"Avenue", "St"/"Street", "Rd"/"Road" as identical; a house number plus
|
||||
the same street is the same place), the same subject/vehicle/case number, or a
|
||||
follow-up beat ("units clearing", "negative contact", "tow en route", "event
|
||||
number 214-201", a status update) to an incident that is only a few minutes
|
||||
idle. The bar for "link" on the same talkgroup is LOW.
|
||||
TALKGROUP: the same or an adjacent location (treat "Ave"/"Avenue",
|
||||
"St"/"Street", "Rd"/"Road" as identical; a house number plus the same street
|
||||
is the same place), the same subject/vehicle/case number, or a follow-up beat
|
||||
("units clearing", "negative contact", "tow en route", "event number
|
||||
214-201", a status update) from units already on it. The bar for "link" on
|
||||
the same talkgroup is LOW for follow-ups.
|
||||
- EXCEPT: a call that REPORTS an event is "new" unless it names the same place
|
||||
or subject as the incident: "another accident", "a second MVA", "getting
|
||||
flagged down for", "just got a report of", "respond to <address>", a
|
||||
reassignment. Patrol units on a shared channel work many jobs back to back,
|
||||
so a unit already on an incident is not evidence that its next job is the
|
||||
same one — especially once that incident has been open a long time (see
|
||||
open:/calls:).
|
||||
- Reserve "new" for a call that clearly describes a DIFFERENT event from every
|
||||
recent incident — a different place, different units, and a different subject,
|
||||
not merely a different transmission about the same job.
|
||||
|
||||
@@ -65,3 +65,16 @@ def test_prompt_incidents_is_most_recently_active_first_and_capped():
|
||||
# falls back to started_at when updated_at is absent, and never raises
|
||||
assert _prompt_incidents([{"incident_id": "a", "started_at": NOW.isoformat()},
|
||||
{"incident_id": "b"}])[0]["incident_id"] == "a"
|
||||
|
||||
|
||||
def test_inc_summary_shows_age_and_size_not_just_idle():
|
||||
# server-26#189: every link resets idle, so a 2.5h catch-all read as fresh.
|
||||
s = _inc_summary({
|
||||
"incident_id": "sink",
|
||||
"started_at": "2026-09-07T05:26:00+00:00",
|
||||
"updated_at": "2026-09-07T07:58:00+00:00",
|
||||
"call_ids": [f"c{n}" for n in range(40)],
|
||||
}, NOW)
|
||||
assert "idle:2min ago" in s
|
||||
assert "open:2h34m" in s
|
||||
assert "calls:40" in s
|
||||
|
||||
Reference in New Issue
Block a user