Spaces:
Sleeping
Sleeping
| """Keyword and structured-field extraction for OCR'd document text. | |
| Domain keyword sets flag "notification" events (things a reviewer would want | |
| alerted on), and field patterns pull structured values (IDs, dates, amounts) | |
| out of the surrounding text so results can be dropped straight into a CSV. | |
| """ | |
| import re | |
| KEYWORD_SETS = { | |
| "construction": [ | |
| "change order", "delay notice", "punch list", "non-conformance", | |
| "rfi", "submittal rejected", "back charge", "backcharge", | |
| "stop work", "safety violation", "material shortage", | |
| "schedule slip", "cost overrun", "field order", "notice to proceed", | |
| ], | |
| "compliance": [ | |
| "non-compliant", "noncompliance", "violation", "deficiency", | |
| "corrective action", "audit finding", "expired", "expiration", | |
| "recall", "citation", "penalty", "out of compliance", | |
| "failed inspection", "not approved", | |
| ], | |
| "finance": [ | |
| "past due", "overdue", "payment rejected", "invoice disputed", | |
| "credit hold", "chargeback", "insufficient funds", "write-off", | |
| "collections", "declined", "late fee", "short paid", | |
| ], | |
| "general": [ | |
| "urgent", "immediate action required", "cancelled", "rejected", | |
| "approved", "pending approval", "revised", "on hold", | |
| ], | |
| } | |
| FIELD_PATTERNS = { | |
| "dates": re.compile( | |
| r"\b(?:\d{1,2}[/-]\d{1,2}[/-]\d{2,4}|\d{4}-\d{2}-\d{2}|" | |
| r"(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\.?\s+" | |
| r"\d{1,2},?\s+\d{4})\b", | |
| re.IGNORECASE, | |
| ), | |
| "amounts": re.compile( | |
| r"\$\s?\d[\d,]*\.?\d{0,2}\b|\bUSD\s?\d[\d,]*\.?\d{0,2}\b", | |
| re.IGNORECASE, | |
| ), | |
| "percentages": re.compile(r"\b\d{1,3}(?:\.\d+)?\s?%"), | |
| "po_numbers": re.compile( | |
| r"\bP\.?\s?O\.?[ \t]*#?[ \t]*[:\-]?[ \t]*((?=[A-Z0-9-]*\d)[A-Z0-9\-]{3,15})\b", | |
| re.IGNORECASE, | |
| ), | |
| "sap_doc_numbers": re.compile( | |
| r"\bSAP[ \t]*(?:Doc(?:ument)?\.?|#)?[ \t]*[:\-]?[ \t]*(\d{8,10})\b|\b(4[5-8]\d{8})\b", | |
| re.IGNORECASE, | |
| ), | |
| "invoice_numbers": re.compile( | |
| r"\bINV(?:OICE)?\.?[ \t]*#?[ \t]*[:\-]?[ \t]*((?=[A-Z0-9-]*\d)[A-Z0-9\-]{3,15})\b", | |
| re.IGNORECASE, | |
| ), | |
| "cert_codes": re.compile( | |
| r"\b(ISO\s?\d{3,5}(?:-\d+)?|OSHA\s?\d{3,4}(?:\.\d+)?|" | |
| r"ASTM\s?[A-Z]?\d{2,4}(?:-\d{2})?|ANSI\s?[A-Z]?\d+(?:\.\d+)?)\b", | |
| re.IGNORECASE, | |
| ), | |
| } | |
| CONTEXT_WINDOW = 60 | |
| def _dedupe(values): | |
| seen = [] | |
| for v in values: | |
| v = v.strip() | |
| if v and v not in seen: | |
| seen.append(v) | |
| return seen | |
| def extract_fields(text): | |
| """Pull structured fields out of a page/block of text.""" | |
| fields = {} | |
| for name, pattern in FIELD_PATTERNS.items(): | |
| matches = pattern.findall(text) | |
| flat = [] | |
| for m in matches: | |
| if isinstance(m, tuple): | |
| flat.extend(g for g in m if g) | |
| else: | |
| flat.append(m) | |
| fields[name] = _dedupe(flat) | |
| return fields | |
| def find_notifications(text, categories, custom_keywords=None): | |
| """Scan text for keyword hits across the given categories. | |
| Returns a list of dicts: {category, keyword, context}. | |
| """ | |
| hits = [] | |
| keywords_by_category = {} | |
| for cat in categories: | |
| keywords_by_category[cat] = list(KEYWORD_SETS.get(cat, [])) | |
| if custom_keywords: | |
| keywords_by_category["custom"] = list(custom_keywords) | |
| lower_text = text.lower() | |
| for category, keywords in keywords_by_category.items(): | |
| for kw in keywords: | |
| kw_clean = kw.strip().lower() | |
| if not kw_clean: | |
| continue | |
| start = 0 | |
| while True: | |
| idx = lower_text.find(kw_clean, start) | |
| if idx == -1: | |
| break | |
| ctx_start = max(0, idx - CONTEXT_WINDOW) | |
| ctx_end = min(len(text), idx + len(kw_clean) + CONTEXT_WINDOW) | |
| context = text[ctx_start:ctx_end].replace("\n", " ").strip() | |
| hits.append({ | |
| "category": category, | |
| "keyword": kw.strip(), | |
| "context": context, | |
| }) | |
| start = idx + len(kw_clean) | |
| return hits | |