Organized

This commit is contained in:
Jason Thistlethwaite
2026-10-17 20:50:35 -04:00
parent bd26c8894f
commit 9ee9eb3ec6
130 changed files with 24172 additions and 140 deletions
+173
View File
@@ -0,0 +1,173 @@
#!/usr/bin/env python3
"""Extract common CS entity identifiers from ticket text (pattern-based MVP).
Does not call NMI. Purpose: surface tracking numbers / order-like IDs / SKUs
so humans and agents can jump systems without copy-paste gymnastics.
Patterns are conservative heuristics — false positives are possible; treat hits
as hints, not ground truth.
"""
from __future__ import annotations
import re
from typing import Any, Iterable
# USPS: 20–22 digit, or 420… + 26 digits, or 13-char ending US
_USPS = re.compile(
r"\b("
r"9[0-9]{19,21}"
r"|420\d{5}\d{20,26}"
r"|[A-Z]{2}\d{9}US"
r")\b",
re.I,
)
_UPS = re.compile(r"\b(1Z[0-9A-Z]{16})\b", re.I)
_FEDEX = re.compile(r"\b(\d{12}|\d{15})\b")
_AMZ_ORDER = re.compile(r"\b(\d{3}-\d{7}-\d{7})\b")
# Prefer digit-heavy order captures; reject pure alpha HTML fragments
_ORDER_LABEL = re.compile(
r"(?:order\s*(?:id|#|number|no\.?)|po\s*(?:#|number)?)\s*[:=\s#]*([A-Z0-9][A-Z0-9\-_/]{3,32})",
re.I,
)
_ITEM_LABEL = re.compile(
r"(?:item\s*(?:#|no\.?|number)|sku|merchant\s*sku)\s*[:=\s#]*([A-Z0-9][A-Z0-9\-_/]{2,40})",
re.I,
)
_ACCOUNT = re.compile(r"\baccount\s*[#:]?\s*([A-Z0-9]{4,20})\b", re.I)
# LDR unit item numbers: 9 digits starting with 2
_ITEM_NO_BARE = re.compile(r"\b(2\d{8})\b")
_ORDER_STOP = {
"number",
"id",
"no",
"none",
"null",
"true",
"false",
"class",
"style",
"mode",
"text",
"title",
"guide",
"html",
"body",
"rted",
"rtal",
"licy",
"lation-mode",
"rtant",
"nsive",
"nsive-text",
"nsive-title",
"nsive-h2",
"ints",
}
def _text_from_issue(issue: dict[str, Any]) -> str:
chunks: list[str] = []
for key in ("subject", "description"):
val = issue.get(key)
if val:
chunks.append(str(val))
helpdesk = issue.get("helpdesk_ticket")
if isinstance(helpdesk, dict):
for key in ("from_address", "to_address", "message_id"):
if helpdesk.get(key):
chunks.append(str(helpdesk[key]))
contact = helpdesk.get("contact")
if isinstance(contact, dict):
for key in ("email", "company", "name", "first_name", "last_name"):
if contact.get(key):
chunks.append(str(contact[key]))
journals = issue.get("journals")
if isinstance(journals, list):
for j in journals[-15:]:
if isinstance(j, dict) and j.get("notes"):
chunks.append(str(j["notes"]))
return "\n".join(chunks)
def extract_entities(text: str) -> list[dict[str, str]]:
"""Return unique {type, value, confidence} hits from free text."""
hits: list[dict[str, str]] = []
seen: set[tuple[str, str]] = set()
def add(kind: str, value: str, confidence: str = "high") -> None:
value = value.strip()
if not value:
return
key = (kind, value.upper())
if key in seen:
return
seen.add(key)
hits.append({"type": kind, "value": value, "confidence": confidence})
for m in _UPS.finditer(text):
add("tracking_ups", m.group(1).upper())
for m in _USPS.finditer(text):
add("tracking_usps", m.group(1).upper())
for m in _AMZ_ORDER.finditer(text):
add("order_amazon", m.group(1))
for m in _ORDER_LABEL.finditer(text):
val = m.group(1)
if re.fullmatch(r"\d{3}-\d{7}-\d{7}", val):
add("order_amazon", val)
continue
if not re.search(r"\d", val):
continue
if val.lower() in _ORDER_STOP:
continue
# Require at least 5 chars and a digit; reject long alpha+token noise
if len(val) < 5:
continue
if re.search(r"[a-zA-Z]{6,}", val) and not re.search(r"\d{4,}", val):
continue # e.g. MGdHZfSeDzi_BhM6 without enough digits
conf = "high" if re.fullmatch(r"\d{5,12}", val) else "medium"
add("order_id", val, confidence=conf)
for m in _ITEM_LABEL.finditer(text):
val = m.group(1)
if re.fullmatch(r"2\d{8}", val):
add("item_no", val, confidence="high")
elif re.search(r"\d", val) or re.search(r"[A-Z]{2,}", val, re.I):
add("item_or_sku", val, confidence="medium")
for m in _ITEM_NO_BARE.finditer(text):
add("item_no", m.group(1), confidence="medium")
for m in _ACCOUNT.finditer(text):
val = m.group(1)
if val.lower() in {"within", "number", "setup", "your", "the"}:
continue
if not re.search(r"\d", val) and len(val) < 6:
continue
add("account_ref", val, confidence="medium")
if re.search(r"\b(fedex|tracking|shipment|shipped|carrier)\b", text, re.I):
for m in _FEDEX.finditer(text):
# skip USPS/item_no already captured lengths that are tracking
val = m.group(1)
if val.startswith("9") and len(val) >= 20:
continue
if re.fullmatch(r"2\d{8}", val):
continue
add("tracking_fedex_candidate", val, confidence="low")
return hits
def extract_entities_from_issue(issue: dict[str, Any]) -> list[dict[str, str]]:
return extract_entities(_text_from_issue(issue))
def format_entities_line(entities: Iterable[dict[str, str]]) -> str:
parts = []
for e in entities:
conf = e.get("confidence") or "high"
if conf == "high":
parts.append(f"{e['type']}:{e['value']}")
else:
parts.append(f"{e['type']}:{e['value']}({conf})")
return "; ".join(parts)