fix(dispatch): presence is three-state — absent status key is unknown, not off-duty
The previous commit treated a missing miler_status:<id> key as "not available". In production only 2 of 34 milers have that key at all, so the agent would have reported "no available rider" for nearly every failure — a confident wrong answer in the opposite direction from the bug it fixed. - _miler_presence returns available / unavailable / unknown. No key, an unparseable value, or an unrecognised status reads as unknown. - _find_zone prefers a confirmed-available miler, otherwise reports the nearest unknown-presence one (it may well be assignable), and returns None only when every nearby candidate is confirmed off duty. - Facts carry nearest_miler_within_km + nearest_miler_presence; the prompt states plainly that unknown presence is not evidence of a coverage gap and should lean to monitor/escalate rather than ops_alert. - New eval case for riders-nearby-but-no-presence-data; tests for all three presence states. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012AJLYcbTHCe45fyFnMfEin
This commit is contained in:
@@ -34,10 +34,18 @@ DISPATCH_STREAM = os.getenv("DISPATCH_STREAM", "")
|
||||
# The LLM is only consulted before the first alert, so cost is bounded per zone.
|
||||
DISPATCH_REALERT_EVERY = int(os.getenv("DISPATCH_REALERT_EVERY", "100"))
|
||||
|
||||
# A miler counts as available only if the Go backend's presence key says so.
|
||||
# The milers:locations geo index keeps last-known positions indefinitely, so
|
||||
# Presence, as reported by the Go backend's miler_status:<id> key. The
|
||||
# milers:locations geo index keeps last-known positions indefinitely, so
|
||||
# membership alone says nothing about whether anyone is actually on duty.
|
||||
#
|
||||
# Three-state on purpose. Only 2 of 34 milers had a status key in production
|
||||
# (2026-09-22), so treating "no key" as off-duty would report "nobody
|
||||
# available" even where the backend can assign — a confident wrong answer.
|
||||
# Absent presence data is reported as unknown and the decision is told so.
|
||||
MILER_AVAILABLE_STATUSES = {"available"}
|
||||
MILER_UNAVAILABLE_STATUSES = {"break", "offline", "busy", "onbreak", "off_duty", "unavailable"}
|
||||
|
||||
AVAILABLE, UNAVAILABLE, UNKNOWN = "available", "unavailable", "unknown"
|
||||
|
||||
|
||||
class DispatchAgent(SpecializedAgent):
|
||||
@@ -138,25 +146,34 @@ class DispatchAgent(SpecializedAgent):
|
||||
# Redis GEO lookup #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
async def _miler_is_available(self, miler_id) -> bool:
|
||||
"""Liveness check against the backend-owned miler_status:<id> key
|
||||
({"userid": .., "status": "Available" | "Break" | ...}). Missing or
|
||||
unparseable → not available: geo-index presence alone is not evidence."""
|
||||
async def _miler_presence(self, miler_id) -> str:
|
||||
"""AVAILABLE / UNAVAILABLE / UNKNOWN from the backend-owned
|
||||
miler_status:<id> key ({"userid": .., "status": "Available"|"Break"|..}).
|
||||
|
||||
No key, an unparseable value, or an unrecognised status → UNKNOWN, never
|
||||
UNAVAILABLE: most milers have no key at all, and absence of evidence is
|
||||
not evidence of absence."""
|
||||
try:
|
||||
raw = await self._redis.get(f"miler_status:{miler_id}")
|
||||
if not raw:
|
||||
return False
|
||||
status = json.loads(raw).get("status", "")
|
||||
return str(status).lower() in MILER_AVAILABLE_STATUSES
|
||||
return UNKNOWN
|
||||
status = str(json.loads(raw).get("status", "")).lower()
|
||||
except Exception:
|
||||
return False
|
||||
return UNKNOWN
|
||||
if status in MILER_AVAILABLE_STATUSES:
|
||||
return AVAILABLE
|
||||
if status in MILER_UNAVAILABLE_STATUSES:
|
||||
return UNAVAILABLE
|
||||
return UNKNOWN
|
||||
|
||||
async def _find_zone(
|
||||
self, lat: float, lon: float, radius_km: int = 10
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""GEORADIUS sweep on milers:locations filtered to milers whose status
|
||||
key says they are available. Returns the nearest available miler (with
|
||||
how many index entries were skipped as stale/off-duty), or None."""
|
||||
"""GEORADIUS sweep on milers:locations, preferring a miler the backend
|
||||
reports Available. If none is confirmed Available but some have no
|
||||
presence data, the nearest of those is returned with presence=UNKNOWN —
|
||||
it may well be assignable. Returns None only when every candidate is
|
||||
confirmed unavailable (or there are none)."""
|
||||
try:
|
||||
results = await self._redis.georadius(
|
||||
"milers:locations",
|
||||
@@ -168,21 +185,32 @@ class DispatchAgent(SpecializedAgent):
|
||||
if not results:
|
||||
return None
|
||||
|
||||
first_unknown = None
|
||||
for candidate in results:
|
||||
if await self._miler_is_available(candidate):
|
||||
miler_info = await self._redis.hgetall(f"miler:{candidate}")
|
||||
return {
|
||||
"miler_id": candidate,
|
||||
"hub_id": miler_info.get("hub_id", "UNKNOWN"),
|
||||
"zone_id": miler_info.get("zone_id", "unknown"),
|
||||
"avg_delivery_time": int(miler_info.get("avg_delivery_time", 60)),
|
||||
"stale_candidates_skipped": len(results) - 1,
|
||||
}
|
||||
presence = await self._miler_presence(candidate)
|
||||
if presence == AVAILABLE:
|
||||
return await self._miler_record(candidate, AVAILABLE, results)
|
||||
if presence == UNKNOWN and first_unknown is None:
|
||||
first_unknown = candidate
|
||||
|
||||
if first_unknown is not None:
|
||||
return await self._miler_record(first_unknown, UNKNOWN, results)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning(f"Redis GEORADIUS error: {e}")
|
||||
return None
|
||||
|
||||
async def _miler_record(self, miler_id, presence, candidates) -> Dict[str, Any]:
|
||||
miler_info = await self._redis.hgetall(f"miler:{miler_id}")
|
||||
return {
|
||||
"miler_id": miler_id,
|
||||
"presence": presence,
|
||||
"hub_id": miler_info.get("hub_id", "UNKNOWN"),
|
||||
"zone_id": miler_info.get("zone_id", "unknown"),
|
||||
"avg_delivery_time": int(miler_info.get("avg_delivery_time", 60)),
|
||||
"candidates_considered": len(candidates),
|
||||
}
|
||||
|
||||
async def _count_in_geo_index(self, lat: float, lon: float, radius_km: int = 30) -> Optional[int]:
|
||||
"""Raw geo-index membership (any status) — reported separately so the
|
||||
decision can tell 'nobody registered here' from 'riders exist but none on duty'."""
|
||||
@@ -327,22 +355,30 @@ class DispatchAgent(SpecializedAgent):
|
||||
facts["has_coordinates"] = has_coords
|
||||
|
||||
nearest_km = None
|
||||
nearest_presence = None
|
||||
if has_coords:
|
||||
for radius in (10, 20, 30):
|
||||
try:
|
||||
if await self._find_zone(lat, lon, radius_km=radius):
|
||||
nearest_km = radius
|
||||
break
|
||||
found = await self._find_zone(lat, lon, radius_km=radius)
|
||||
except Exception as e:
|
||||
logger.warning(f"assignment facts: GEORADIUS {radius}km failed: {e}")
|
||||
break
|
||||
if found:
|
||||
nearest_km = radius
|
||||
nearest_presence = found.get("presence", UNKNOWN)
|
||||
break
|
||||
|
||||
if not has_coords:
|
||||
facts["nearest_available_miler_within_km"] = "unknown (no coordinates)"
|
||||
facts["nearest_miler_within_km"] = "unknown (no coordinates)"
|
||||
facts["nearest_miler_presence"] = "unknown (no coordinates)"
|
||||
elif nearest_km is None:
|
||||
facts["nearest_available_miler_within_km"] = "none within 30km"
|
||||
facts["nearest_miler_within_km"] = "none within 30km"
|
||||
facts["nearest_miler_presence"] = "none found"
|
||||
else:
|
||||
facts["nearest_available_miler_within_km"] = nearest_km
|
||||
facts["nearest_miler_within_km"] = nearest_km
|
||||
# "available" = backend confirms on duty; "unknown" = no presence
|
||||
# data for any nearby miler, so this is not evidence of a gap.
|
||||
facts["nearest_miler_presence"] = nearest_presence
|
||||
|
||||
# Distinguish "no riders registered here" from "riders exist, none on duty".
|
||||
if has_coords:
|
||||
|
||||
Reference in New Issue
Block a user