mirror of
https://github.com/Sea-Haven-Industries/apm-wo-analysis.git
synced 2026-09-30 07:43:15 +00:00
77 lines
2.3 KiB
Python
77 lines
2.3 KiB
Python
|
|
"""Two-axis APM work-order classifier — the core domain logic.
|
||
|
|
|
||
|
|
Axis 1 — comment intent: regex over the HTML-stripped ``Last Comment``,
|
||
|
|
most-specific first.
|
||
|
|
Axis 2 — structured state: ``Hold Reason`` + ``WO Status``.
|
||
|
|
|
||
|
|
Resolution: comment intent wins when confident, else structured state, else
|
||
|
|
``Other``. A Claude Haiku fallback (Secrets Manager
|
||
|
|
``apm-wo-analysis/anthropic-api-key``) is reserved strictly for ambiguous
|
||
|
|
free-text with no structured signal. A mismatch detector flags when comment
|
||
|
|
intent contradicts structured state — surface, never suppress.
|
||
|
|
|
||
|
|
The authoritative spec is CLAUDE.md ("The classification model"). This module
|
||
|
|
is owned by the classifier-engineer agent; the rule ladder and Haiku fallback
|
||
|
|
are implemented in Phase 2 (docs/BUILD.md) and smoke-tested against a real
|
||
|
|
export before merge.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
# Axis 2 — Hold Reason → category.
|
||
|
|
HOLD_TO_CATEGORY = {
|
||
|
|
"SCHEDULING": "Awaiting Scheduling",
|
||
|
|
"REPORT": "Report / Docs Needed",
|
||
|
|
"VENDOR": "Awaiting Vendor / Parts",
|
||
|
|
"PARTS": "Awaiting Vendor / Parts",
|
||
|
|
"ORDER": "Awaiting Vendor / Parts",
|
||
|
|
"VERIFY": "Verification Needed",
|
||
|
|
"RESOURCE": "Resource Hold",
|
||
|
|
"NOEQUIP": "No Equipment",
|
||
|
|
}
|
||
|
|
|
||
|
|
# Axis 2 — WO Status signals.
|
||
|
|
WO_STATUS_CANCELLED = "RCAN" # → Cancelled
|
||
|
|
WO_STATUS_HOLD = "H" # corroborates On Hold
|
||
|
|
WO_STATUS_IN_FLIGHT = frozenset({"IP", "R", "RR"})
|
||
|
|
|
||
|
|
ESCALATION_CATEGORIES = frozenset(
|
||
|
|
{
|
||
|
|
"1st Escalation",
|
||
|
|
"2nd Escalation",
|
||
|
|
"3rd Escalation",
|
||
|
|
"SIM Ticket",
|
||
|
|
"Other Escalation",
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
ACTION_NEEDED_CATEGORIES = ESCALATION_CATEGORIES | frozenset(
|
||
|
|
{
|
||
|
|
"Awaiting Scheduling",
|
||
|
|
"Report / Docs Needed",
|
||
|
|
"Awaiting Report / Invoice",
|
||
|
|
"Awaiting Vendor / Parts",
|
||
|
|
"Status Inquiry",
|
||
|
|
"Vendor No-Show",
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def strip_html(comment: str | None) -> str:
|
||
|
|
"""Unwrap the HTML-wrapped Last Comment and decode entities. (Phase 2)"""
|
||
|
|
raise NotImplementedError
|
||
|
|
|
||
|
|
|
||
|
|
def comment_intent(comment: str) -> str | None:
|
||
|
|
"""Ordered, most-specific-first rule ladder; returns a bucket or None. (Phase 2)"""
|
||
|
|
raise NotImplementedError
|
||
|
|
|
||
|
|
|
||
|
|
def classify(
|
||
|
|
wo_status: str | None,
|
||
|
|
hold_reason: str | None,
|
||
|
|
last_comment: str | None,
|
||
|
|
) -> tuple[str, str | None]:
|
||
|
|
"""Resolve to ``(final_category, mismatch_reason | None)``. (Phase 2)"""
|
||
|
|
raise NotImplementedError
|