gaurv007 commited on
Commit
423d2a9
·
verified ·
1 Parent(s): 5786572

fix(v4.3): app.py — bug report fixes (10 issues)

Browse files
Files changed (1) hide show
  1. app.py +98 -6
app.py CHANGED
@@ -617,12 +617,51 @@ _LABEL_GUARDRAILS = {
617
  r'uncapped|unlimited.{0,10}liabilit|no.{0,10}(limit|cap).{0,10}liabilit',
618
  re.IGNORECASE
619
  ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
620
  }
621
 
622
  def _apply_guardrails(label, text, confidence):
 
 
 
 
 
 
623
  guard = _LABEL_GUARDRAILS.get(label)
624
  if guard and not guard.search(text):
625
  return "Other", confidence * 0.3
 
 
 
 
 
626
  return label, confidence
627
 
628
  def _text_hash(text):
@@ -951,6 +990,47 @@ def extract_entities(text):
951
  else:
952
  entities = _extract_entities_regex(text)
953
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
954
  # Always supplement with regex patterns for things NER often misses
955
  regex_ents = _extract_entities_regex(text)
956
  ml_spans = set()
@@ -1176,14 +1256,26 @@ def compute_risk_score(clause_results, total_clauses):
1176
  if total_clauses == 0:
1177
  return 0, "A", sev_counts
1178
 
1179
- # FIX v4.1: Absolute risk — critical findings should always score high
1180
- # regardless of document size. A 200-clause doc with 5 critical findings
1181
- # is just as dangerous as a 10-clause doc with 5 critical findings.
 
 
 
 
 
 
1182
  weighted = sum(sev_counts[s] * RISK_WEIGHTS[s] for s in sev_counts)
1183
 
1184
- # Diminishing returns formula: starts linear, flattens near 100
1185
- # max theoretical = 100, one CRITICAL finding = ~30, two = ~48, five = ~72
1186
- risk = min(100, round(100 * (1 - (1 / (1 + weighted / 30)))))
 
 
 
 
 
 
1187
 
1188
  if risk >= 70: grade = "F"
1189
  elif risk >= 50: grade = "D"
 
617
  r'uncapped|unlimited.{0,10}liabilit|no.{0,10}(limit|cap).{0,10}liabilit',
618
  re.IGNORECASE
619
  ),
620
+ # FIX v4.3: ROFR fires on "right, title, and interest" in IP clauses — require ROFR-specific phrases
621
+ "ROFR/ROFO/ROFN": re.compile(
622
+ r'right\s+of\s+first\s+(?:refusal|offer|negotiation)|ROFR|ROFO|ROFN',
623
+ re.IGNORECASE
624
+ ),
625
+ # FIX v4.3: Renewal Term fires on "twelve (12) months" in liability caps — require renewal-specific phrases
626
+ "Renewal Term": re.compile(
627
+ r'renew(?:al)?|successive\s+term|auto(?:matic(?:ally)?)?\s*[\-\s]?renew|non[\-\s]?renewal',
628
+ re.IGNORECASE
629
+ ),
630
+ }
631
+
632
+ # FIX v4.3: Exclusion patterns — even if guardrail passes, exclude if contra-indicators present
633
+ _LABEL_EXCLUSIONS = {
634
+ "ROFR/ROFO/ROFN": re.compile(
635
+ r'assigns?\s+to|irrevocab(?:ly|le)\s+assign|all\s+right,?\s+title,?\s+and\s+interest|work[\-\s]for[\-\s]hire',
636
+ re.IGNORECASE
637
+ ),
638
+ "Renewal Term": re.compile(
639
+ r'limitation\s+of\s+liabilit|shall\s+not\s+be\s+liable|indemnif|hold\s+harmless|defend\s+and',
640
+ re.IGNORECASE
641
+ ),
642
+ }
643
+
644
+ # FIX v4.3: Minimum confidence thresholds per label (overrides the per-class _CUAD_THRESHOLDS)
645
+ _LABEL_MIN_CONFIDENCE = {
646
+ "ROFR/ROFO/ROFN": 0.65,
647
+ "Renewal Term": 0.70,
648
  }
649
 
650
  def _apply_guardrails(label, text, confidence):
651
+ # Check minimum confidence for specific labels
652
+ min_conf = _LABEL_MIN_CONFIDENCE.get(label)
653
+ if min_conf and confidence < min_conf:
654
+ return "Other", confidence * 0.2
655
+
656
+ # Check required keywords (must be present)
657
  guard = _LABEL_GUARDRAILS.get(label)
658
  if guard and not guard.search(text):
659
  return "Other", confidence * 0.3
660
+
661
+ # Check exclusion patterns (must NOT be present)
662
+ exclusion = _LABEL_EXCLUSIONS.get(label)
663
+ if exclusion and exclusion.search(text):
664
+ return "Other", confidence * 0.2
665
  return label, confidence
666
 
667
  def _text_hash(text):
 
990
  else:
991
  entities = _extract_entities_regex(text)
992
 
993
+ # FIX v4.3: Post-process ML entities to clean up WordPiece artefacts
994
+ cleaned_entities = []
995
+ for e in entities:
996
+ text_val = e.get("text", "")
997
+ # Strip WordPiece subword tokens (## prefix)
998
+ if "##" in text_val:
999
+ text_val = re.sub(r'##\w*', '', text_val).strip()
1000
+ text_val = re.sub(r'\s+', ' ', text_val).strip()
1001
+ # Discard entities that are too short, start/end with hyphens, or are garbled
1002
+ if len(text_val) < 2:
1003
+ continue
1004
+ if text_val.startswith("-") or text_val.endswith("-"):
1005
+ continue
1006
+ # Discard low-confidence MISC entities (almost always tokenisation artefacts)
1007
+ if e.get("type") == "MISC" and e.get("score", 1.0) < 0.6:
1008
+ continue
1009
+ # Discard entities that are mostly punctuation/symbols
1010
+ alpha_ratio = sum(1 for c in text_val if c.isalnum()) / max(len(text_val), 1)
1011
+ if alpha_ratio < 0.4:
1012
+ continue
1013
+ e["text"] = text_val
1014
+ cleaned_entities.append(e)
1015
+ entities = cleaned_entities
1016
+
1017
+ # FIX v4.3: Split concatenated MONEY/QUANTITY entities
1018
+ # e.g., "usd $ 485, 000,usd $ 72, 000" → separate entities
1019
+ _CURRENCY_SPLIT = re.compile(r'(?<=[\d,])\s*(?=(?:USD|usd|EUR|GBP|\$|£|€))', re.IGNORECASE)
1020
+ split_entities = []
1021
+ for e in entities:
1022
+ if e.get("type") in ("MONEY", "QUANTITY") and _CURRENCY_SPLIT.search(e["text"]):
1023
+ parts = _CURRENCY_SPLIT.split(e["text"])
1024
+ for part in parts:
1025
+ part = part.strip().strip(",").strip()
1026
+ if len(part) >= 2:
1027
+ new_ent = dict(e)
1028
+ new_ent["text"] = re.sub(r'\s+', '', part) if "$" in part or "USD" in part.upper() else part
1029
+ split_entities.append(new_ent)
1030
+ else:
1031
+ split_entities.append(e)
1032
+ entities = split_entities
1033
+
1034
  # Always supplement with regex patterns for things NER often misses
1035
  regex_ents = _extract_entities_regex(text)
1036
  ml_spans = set()
 
1256
  if total_clauses == 0:
1257
  return 0, "A", sev_counts
1258
 
1259
+ # FIX v4.3: Revised risk formula scale denominator with clause count
1260
+ # to prevent small contracts from always scoring 80+.
1261
+ # The old formula used a fixed /30 denominator which meant even 2 CRITICAL
1262
+ # flags scored 73, making almost every contract grade F.
1263
+ #
1264
+ # New approach: dynamic denominator based on total clauses analysed.
1265
+ # This means risk is relative to document complexity.
1266
+ # - 1 CRITICAL in 5 clauses = high risk
1267
+ # - 1 CRITICAL in 50 clauses = moderate risk (proportionally less of the contract)
1268
  weighted = sum(sev_counts[s] * RISK_WEIGHTS[s] for s in sev_counts)
1269
 
1270
+ # Dynamic max: what if every clause were CRITICAL?
1271
+ max_possible = total_clauses * RISK_WEIGHTS["CRITICAL"]
1272
+ if max_possible == 0:
1273
+ max_possible = 1
1274
+
1275
+ # Blend: 60% absolute (diminishing returns) + 40% relative (to total clauses)
1276
+ absolute_risk = 100 * (1 - (1 / (1 + weighted / 50))) # /50 instead of /30 = softer curve
1277
+ relative_risk = min(100, (weighted / max_possible) * 100)
1278
+ risk = min(100, round(0.6 * absolute_risk + 0.4 * relative_risk))
1279
 
1280
  if risk >= 70: grade = "F"
1281
  elif risk >= 50: grade = "D"