Spaces:
Sleeping
Sleeping
| """Renderer/judge for normalized dispatch phraseology. | |
| This repository originally shipped an ATC phraseology module. After the domain | |
| pivot to 911 dispatch, we keep the same public symbols (`PhraseologyRenderer`, | |
| `PhraseologyJudge`) but implement dispatch-oriented rendering and scoring. | |
| """ | |
| from __future__ import annotations | |
| from pydantic import BaseModel | |
| from src.models import Action, DispatchAction | |
| class PhraseologyRenderer(BaseModel): | |
| """Converts structured dispatch actions to standardized strings.""" | |
| def render(self, action: Action) -> str: | |
| if action.action_type == DispatchAction.DISPATCH: | |
| return f"DISPATCH {action.unit_id} -> {action.incident_id}" | |
| if action.action_type == DispatchAction.REASSIGN: | |
| return f"REASSIGN {action.unit_id} -> {action.incident_id}" | |
| if action.action_type == DispatchAction.CANCEL: | |
| return f"CANCEL {action.unit_id} FROM {action.incident_id}" | |
| if action.action_type == DispatchAction.STAGE: | |
| return f"STAGE {action.unit_id} FOR {action.incident_id}" | |
| if action.action_type == DispatchAction.MUTUAL_AID: | |
| return f"MUTUAL_AID {action.unit_id} -> {action.incident_id}" | |
| if action.action_type in {DispatchAction.UPGRADE, DispatchAction.DOWNGRADE}: | |
| if action.priority_override is None: | |
| return f"INVALID: {action.action_type.value} requires priority_override" | |
| return ( | |
| f"{action.action_type.value} {action.incident_id} " | |
| f"TO {action.priority_override.value}" | |
| ) | |
| return f"INVALID: unknown action_type {action.action_type}" | |
| class PhraseologyJudge(BaseModel): | |
| """Scores candidate phraseology against structured action truth.""" | |
| def _tokenize(self, text: str) -> set[str]: | |
| import re | |
| # Treat common dispatch IDs like "MED-1" and "INC-001" as single tokens. | |
| return set(re.findall(r"[a-z0-9]+(?:-[a-z0-9]+)*", text.lower())) | |
| def score(self, ground_truth_action: Action, candidate_text: str) -> float: | |
| canonical = PhraseologyRenderer().render(ground_truth_action) | |
| norm_candidate = candidate_text.strip().lower() | |
| norm_canonical = canonical.strip().lower() | |
| if norm_candidate == norm_canonical: | |
| return 1.0 | |
| canonical_tokens = self._tokenize(canonical) | |
| candidate_tokens = self._tokenize(candidate_text) | |
| overlap = canonical_tokens & candidate_tokens | |
| if not overlap: | |
| return 0.0 | |
| return 0.5 if (len(overlap) / max(len(canonical_tokens), 1)) >= 0.5 else 0.0 | |
| def check_readback(self, candidate_text: str, ground_truth_action: Action) -> bool: | |
| tokens = self._tokenize(candidate_text) | |
| if ground_truth_action.action_type in {DispatchAction.DISPATCH, DispatchAction.REASSIGN}: | |
| return ( | |
| ground_truth_action.unit_id.lower() in tokens | |
| and ground_truth_action.incident_id.lower() in tokens | |
| ) | |
| if ground_truth_action.action_type == DispatchAction.CANCEL: | |
| return ( | |
| ground_truth_action.unit_id.lower() in tokens | |
| and ground_truth_action.incident_id.lower() in tokens | |
| ) | |
| if ground_truth_action.action_type in {DispatchAction.UPGRADE, DispatchAction.DOWNGRADE}: | |
| if ground_truth_action.priority_override is None: | |
| return False | |
| return ( | |
| ground_truth_action.incident_id.lower() in tokens | |
| and ground_truth_action.priority_override.value.lower() in tokens | |
| ) | |
| return True | |