"""NLP entity extraction — no LLM, pure regex/heuristic. Extracts: parties, dates, monetary amounts, durations, jurisdictions, key obligations. Runs synchronously on the parsed document text before any LLM calls. """ from __future__ import annotations import re from app.schemas import DocumentEntities # --- Pattern sets --- _PARTY_PATTERNS = [ r'\b(?:the\s+)?(?:Company|Employer|Client|Contractor|Vendor|Provider|Lessor|Landlord|' r'Licensor|Disclosing\s+Party|Receiving\s+Party|Service\s+Provider)\b', r'\b(?:the\s+)?(?:Employee|Consultant|Freelancer|Tenant|Lessee|Licensee|' r'Customer|User|Subscriber)\b', ] _DATE_PATTERNS = [ r'\b(?:January|February|March|April|May|June|July|August|September|October|November|December)' r'\s+\d{1,2},?\s+\d{4}\b', r'\b\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}\b', r'\beffective\s+(?:date|as\s+of)\s+[^\.,;]{3,30}', ] _MONEY_PATTERNS = [ r'\$\s*[\d,]+(?:\.\d{2})?(?:\s*(?:million|billion|thousand|USD|CAD|EUR|GBP))?', r'\b[\d,]+(?:\.\d{2})?\s*(?:US\s*)?dollars?\b', r'\b(?:USD|EUR|GBP|CAD)\s*[\d,]+(?:\.\d{2})?\b', ] _DURATION_PATTERNS = [ r'\b(?:zero\s*\(0\)|one\s*\(1\)|two\s*\(2\)|three\s*\(3\)|four\s*\(4\)|' r'five\s*\(5\)|six\s*\(6\)|seven\s*\(7\)|eight\s*\(8\)|nine\s*\(9\)|' r'ten\s*\(10\)|twelve\s*\(12\)|eighteen\s*\(18\)|twenty-?four\s*\(24\)|' r'thirty\s*\(30\)|sixty\s*\(60\)|ninety\s*\(90\)|one\s+hundred\s+eighty\s*\(180\))\s+' r'(?:calendar\s+)?(?:days?|weeks?|months?|years?)\b', r'\b\d+\s*(?:calendar\s+)?(?:days?|weeks?|months?|years?)\b', r'\b(?:perpetual|irrevocable|indefinite(?:ly)?|forever|permanent(?:ly)?)\b', ] _JURISDICTION_PATTERNS = [ r'\bState\s+of\s+[A-Z][a-z]+(?:\s+[A-Z][a-z]+)?\b', r'\b(?:California|Delaware|New\s+York|Texas|Florida|Illinois|Washington|' r'Colorado|Massachusetts|Georgia)\b', r'\b(?:United\s+States|federal|U\.S\.|EU|European\s+Union|GDPR|CCPA)\b', ] _OBLIGATION_KEYWORDS = [ (r'\bshall\s+not\b', "Prohibition"), (r'\bmust\s+not\b', "Prohibition"), (r'\bhereby\s+assigns?\b', "Assignment of rights"), (r'\bwaives?\s+(?:all|any)\b', "Waiver of rights"), (r'\birrevocably\b', "Irrevocable commitment"), (r'\bin\s+perpetuity\b', "Perpetual obligation"), (r'\bsole\s+(?:discretion|remedy)\b', "Unilateral discretion"), (r'\bclass\s+action\s+waiver\b', "Class action waiver"), (r'\bbinding\s+arbitration\b', "Mandatory arbitration"), (r'\bat.?will\b', "At-will relationship"), (r'\bauto.?renew(?:al|s)?\b', "Auto-renewal clause"), ] def _dedupe(items: list[str]) -> list[str]: seen: set[str] = set() out: list[str] = [] for item in items: key = item.strip().lower() if key not in seen and len(key) > 1: seen.add(key) out.append(item.strip()) return out def extract_entities(text: str) -> DocumentEntities: parties: list[str] = [] for pat in _PARTY_PATTERNS: parties += re.findall(pat, text, re.IGNORECASE) parties = _dedupe([p.replace("the ", "").strip().title() for p in parties])[:8] dates: list[str] = [] for pat in _DATE_PATTERNS: dates += re.findall(pat, text, re.IGNORECASE) dates = _dedupe(dates)[:6] amounts: list[str] = [] for pat in _MONEY_PATTERNS: amounts += re.findall(pat, text, re.IGNORECASE) amounts = _dedupe(amounts)[:8] durations: list[str] = [] for pat in _DURATION_PATTERNS: durations += re.findall(pat, text, re.IGNORECASE) durations = _dedupe(durations)[:8] jurisdictions: list[str] = [] for pat in _JURISDICTION_PATTERNS: jurisdictions += re.findall(pat, text, re.IGNORECASE) jurisdictions = _dedupe(jurisdictions)[:6] obligations: list[str] = [] for pat, label in _OBLIGATION_KEYWORDS: if re.search(pat, text, re.IGNORECASE): obligations.append(label) obligations = _dedupe(obligations)[:6] return DocumentEntities( parties=parties, dates=dates, monetary_amounts=amounts, durations=durations, jurisdictions=jurisdictions, key_obligations=obligations, )