apm-wo-analysis/lambdas/classifier/classify.py
Adam Moussa 58b91bda70 Scaffold apm-wo-analysis repository
Stand up the Phase 0 CDK scaffold for the daily APM work-order
analysis pipeline: two-stack CDK app (pipeline + grafana), classifier
and slack-post Lambda packages, dashboards-as-code, the local
drop-folder uploader, and a classifier smoke-test placeholder.

Wire CI/CD to the org reusable workflows: ci.yaml -> ci-python-sam
(ruff + cdk synth) and deploy.yaml -> cd-cdk (OIDC, cdk deploy --all).
Pin aws-cdk-lib==2.253.1; Lambdas target Python 3.12 / arm64.

Rewrite .gitignore to the org Python-CDK standard so the source-of-
truth files (CLAUDE.md, docs/, .claude/agents) are tracked while build
artifacts (.venv, cdk.out, caches) stay ignored.

Domain logic, stack resources, and dashboards are stubbed and filled
in across Phases 1-5 (docs/BUILD.md). cdk synth is green for both
stacks; ruff check/format pass.
2026-05-28 16:13:13 -04:00

76 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