correlator: address #116 review — call talkgroup id in the prompt, sort candidates

drb-correlation-review: ship, with two bounds the low-bar link rule needs.

1. _call_block emitted only the talkgroup NAME while _inc_summary emits
   numeric tg ids, so the "same talkgroup" precondition in _RULES was
   unevaluable and the low link bar applied unconditionally. _call_block now
   prints "Talkgroup: <name> (id <n>)".
2. ctx["recent"] is an unordered Firestore slice with no order_by; a busy 2h
   window (~40 active incidents) showed the model an arbitrary half of the
   candidates. _prompt_incidents() sorts by updated_at desc before the [:20]
   cap — also makes each row's idle: field monotonic.

+2 tests. Full c2-core suite green (sandboxed venv).

Review follow-ups (not blockers): _parse_response demotes an unresolvable
link to orphan (drops the call) rather than falling back to rules — now on
rising link volume; the 45% tiebreak escalation rate / smart-model cost is
untouched; _ROAD_RE swallows leading tokens so "10 Parker Street" still
won't road-overlap "Parker St".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Logan Cusano
2026-09-07 16:59:29 -04:00
co-authored by Claude Sonnet 5
parent 3a944f35c1
commit 1a631d65d0
2 changed files with 36 additions and 5 deletions
+22 -4
View File
@@ -91,11 +91,29 @@ 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["talkgroup_name"]:
lines.append(f"Talkgroup: {ctx['talkgroup_name']}")
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
# (server-26#115 review).
tgid = ctx.get("talkgroup_id")
name = ctx["talkgroup_name"] or "?"
lines.append(f"Talkgroup: {name}" + (f" (id {tgid})" if tgid is not None else ""))
return "\n".join(lines) if lines else "(no details)"
def _prompt_incidents(recent: list[dict]) -> list[dict]:
"""The ≤20 candidates shown to the model, most-recently-active first.
`ctx["recent"]` is an unordered slice of a Firestore result with no
order_by, so a busy 2h window (~40 active incidents) meant the model saw
an arbitrary half of the candidates (server-26#115 review). Sorting by
updated_at desc also makes each row's `idle:` field monotonic.
"""
def _key(inc: dict):
return str(inc.get("updated_at") or inc.get("started_at") or "")
return sorted(recent, key=_key, reverse=True)[:20]
_SCHEMA = '{"action": "link" | "new" | "orphan", "incident_id": "<id_string or null>", "reasoning": "<one sentence>"}'
_RULES = """
@@ -125,7 +143,7 @@ def _build_decide_prompt(ctx: dict) -> str:
now = ctx["now"]
recent = ctx["recent"]
inc_block = (
"\n".join(_inc_summary(inc, now) for inc in recent[:20])
"\n".join(_inc_summary(inc, now) for inc in _prompt_incidents(recent))
if recent else "(none)"
)
return (
@@ -143,7 +161,7 @@ def _build_tiebreak_prompt(rules_decision: dict, llm_decision: dict, ctx: dict)
now = ctx["now"]
recent = ctx["recent"]
inc_block = (
"\n".join(_inc_summary(inc, now) for inc in recent[:20])
"\n".join(_inc_summary(inc, now) for inc in _prompt_incidents(recent))
if recent else "(none)"
)