FatimaDataScientist72 commited on
Commit
391d758
Β·
verified Β·
1 Parent(s): f989668

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +172 -8
app.py CHANGED
@@ -48,12 +48,19 @@ from huggingface_hub import hf_hub_download
48
  # alongside this file in the same Space for this import to work.
49
  from structural_patterns import scan_structural_patterns, structural_flags_to_dict
50
 
 
 
 
 
 
 
51
  # ── App ────────────────────────────────────────────────────────
52
  app = FastAPI(
53
  title = "MACI β€” Muslim AI Content Intelligence",
54
  description = "Shariah compliance classifier v5.5 β€” institutional grade. "
55
  "Full Terry v0.1 Signal Packet + Phase 2 Maqasid enrichment "
56
- "+ Phase 3 jurisdiction overlay + Phase 4 structural patterns.",
 
57
  version = "5.5",
58
  )
59
  app.add_middleware(CORSMiddleware, allow_origins=["*"],
@@ -134,6 +141,23 @@ SEV = {
134
  (7,"HIGH"):"HIGH", (7,"MEDIUM"):"MEDIUM", (7,"LOW"):"LOW",
135
  }
136
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  # ── Phase 2: Maqasid + consequence ────────────────────────────
138
  CONSEQUENCE_TABLE = {
139
  0:{"maqasid_pillar":"Hifz al-Mal","maqasid_meaning":"Protection of Wealth",
@@ -614,6 +638,118 @@ def classify(text: str, context: str = "general",
614
  if structural_override_reason:
615
  review_reason = structural_override_reason
616
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
617
  # Phase 2 enrichment
618
  conseq = CONSEQUENCE_TABLE[pred]
619
  altmap = ALTERNATIVE_MAP[pred]
@@ -624,6 +760,21 @@ def classify(text: str, context: str = "general",
624
  if partial and not disclaimer:
625
  disclaimer = "Farsi/Tajik input β€” partial coverage. Scholar review recommended."
626
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
627
  # Phase 3 jurisdiction
628
  juris_data = JURISDICTION_EXPOSURE.get(pred, {})
629
  jurisdiction_recognized = jurisdiction in juris_data
@@ -675,9 +826,9 @@ def classify(text: str, context: str = "general",
675
  "economic_signal" : econ_signal,
676
  "action_required" : (conseq["action_required"]
677
  if tier=="HIGH" else "human_review_first"),
678
- "compliant_alternative": altmap["primary"],
679
- "structural_fix" : altmap["structural_fix"],
680
- "aaoifi_standard" : altmap["aaoifi_standard"],
681
  },
682
 
683
  # Phase 3: Jurisdiction
@@ -698,7 +849,7 @@ def classify(text: str, context: str = "general",
698
  } if pred != 0 else "compliant β€” no exposure",
699
  },
700
 
701
- # NEW β€” Phase 4: Structural pattern layer
702
  "structural_pattern_flags": {
703
  "layer_type": "rule-based structural detection (Phase 4)",
704
  "note": (
@@ -711,12 +862,17 @@ def classify(text: str, context: str = "general",
711
  "1500-char truncation applied to the ML layer below. Coverage is "
712
  "limited to the patterns currently defined β€” absence of a flag "
713
  "here is NOT confirmation of structural compliance, only that none "
714
- "of the currently-modeled red-flag patterns matched."
 
 
715
  ),
716
  "flags": structural_flags_list,
717
  "flag_count": len(structural_flags_list),
718
  },
719
 
 
 
 
720
  # Overclaiming controls
721
  "overclaiming_controls" : {
722
  "severity_is_confidence_modulated": True,
@@ -728,6 +884,8 @@ def classify(text: str, context: str = "general",
728
  "original_char_count" : original_length,
729
  "structural_layer_overrode_ml_verdict": bool(structural_override_reason),
730
  "structural_override_reason": structural_override_reason,
 
 
731
  },
732
 
733
  "boundary_behavior" : {
@@ -769,6 +927,7 @@ def classify(text: str, context: str = "general",
769
  "exposure" : juris_info.get("exposure","unknown"),
770
  "truncated" : was_truncated,
771
  "structural_flags": len(structural_flags_list),
 
772
  "audit_hash" : f"sha256:{phash[:16]}...",
773
  },
774
 
@@ -794,6 +953,10 @@ def root():
794
  "(Bay' al-Inah, disguised Riba late-fee, "
795
  "unilateral price variation, risk/ownership "
796
  "decoupling, total liability waiver)",
 
 
 
 
797
  "endpoints" : {
798
  "single" : "POST /api/v1/classify",
799
  "batch" : "POST /api/v1/classify/batch",
@@ -817,6 +980,7 @@ def health():
817
  "ml_f1" : 0.9026,
818
  "rulings" : len(STATE.get("rulings",[])),
819
  "structural_pattern_layer": "active",
 
820
  }
821
 
822
  @app.post(
@@ -830,7 +994,7 @@ def health():
830
  "Returns the full MACI-0.4 signal packet, including "
831
  "maqasid_enrichment (AAOIFI standard, structural fix, "
832
  "compliant alternative), jurisdiction_overlay, "
833
- "structural_pattern_flags, and overclaiming_controls."
834
  )
835
  def classify_endpoint(req: ClassifyRequest):
836
  if "xlmr" not in STATE:
@@ -982,4 +1146,4 @@ def classify_audit(req: AuditRequest):
982
  "mechanism_results": results,
983
 
984
  "audit_hash" : f"sha256:{audit_hash}",
985
- }
 
48
  # alongside this file in the same Space for this import to work.
49
  from structural_patterns import scan_structural_patterns, structural_flags_to_dict
50
 
51
+ # NEW: Phase 5 reconciliation layer β€” upload reconciliation.py alongside
52
+ # this file too. Only scan_hard_negatives is used here; the structural
53
+ # override itself is handled inline below because it needs to update
54
+ # `pred` before Phase 2/3 enrichment run later in this same function.
55
+ from reconciliation import scan_hard_negatives
56
+
57
  # ── App ────────────────────────────────────────────────────────
58
  app = FastAPI(
59
  title = "MACI β€” Muslim AI Content Intelligence",
60
  description = "Shariah compliance classifier v5.5 β€” institutional grade. "
61
  "Full Terry v0.1 Signal Packet + Phase 2 Maqasid enrichment "
62
+ "+ Phase 3 jurisdiction overlay + Phase 4 structural patterns "
63
+ "+ Phase 5 reconciliation.",
64
  version = "5.5",
65
  )
66
  app.add_middleware(CORSMiddleware, allow_origins=["*"],
 
141
  (7,"HIGH"):"HIGH", (7,"MEDIUM"):"MEDIUM", (7,"LOW"):"LOW",
142
  }
143
 
144
+ # NEW (Phase 5): maps each structural pattern to the closest existing
145
+ # `pred` class id. This is what lets a structural override also correct
146
+ # maqasid_enrichment (Phase 2) and jurisdiction_overlay (Phase 3) below β€”
147
+ # both of those blocks key off `pred`, and both run AFTER this mapping is
148
+ # applied, so getting this mapping right means the whole rest of the
149
+ # packet (pillar, AAOIFI standard, jurisdiction exposure/penalty) becomes
150
+ # consistent with the corrected verdict automatically, not just the
151
+ # top-line violation_class field.
152
+ STRUCTURAL_PATTERN_TO_PRED = {
153
+ "SP-01": 1, # Bay' al-Inah buy-back -> Riba (disguised loan)
154
+ "SP-02": 1, # Disguised Riba late-payment-income -> Riba
155
+ "SP-03": 2, # Unilateral price variation -> Gharar
156
+ "SP-04": 2, # Risk/ownership decoupling -> Gharar
157
+ "SP-05": 2, # Total liability waiver -> Gharar (closest of the 8 classes)
158
+ }
159
+ STRUCTURAL_SEVERITY_RANK = {"LOW":1, "MEDIUM":2, "HIGH":3, "CRITICAL":4}
160
+
161
  # ── Phase 2: Maqasid + consequence ────────────────────────────
162
  CONSEQUENCE_TABLE = {
163
  0:{"maqasid_pillar":"Hifz al-Mal","maqasid_meaning":"Protection of Wealth",
 
638
  if structural_override_reason:
639
  review_reason = structural_override_reason
640
 
641
+ # ══════════════════════════════════════════════════════════
642
+ # NEW β€” PHASE 5: RECONCILIATION LAYER
643
+ #
644
+ # The Phase 4 block above only ever touched `recommendation` β€”
645
+ # the top-line `violation_class` / `severity` / `result` fields
646
+ # that the frontend actually displays still came straight from
647
+ # the ML layer untouched. That's what caused the demo to show
648
+ # "Authentic / NONE" on a real Arabic Bay'-al-Inah buy-back and
649
+ # a real Urdu risk/ownership-decoupling clause even though the
650
+ # structural layer had already matched them correctly β€” the
651
+ # match existed in structural_pattern_flags, but nothing
652
+ # upstream of it was told to care.
653
+ #
654
+ # This block does two things, and only these two things:
655
+ #
656
+ # (a) STRUCTURAL OVERRIDE β€” if a structural pattern matched,
657
+ # it now overrides pred/label/conf/tier/verdict/terry/
658
+ # severity themselves, not just recommendation. Because
659
+ # this runs BEFORE the Phase 2/3 enrichment sections below
660
+ # (which key off `pred`), the pillar, AAOIFI standard, and
661
+ # jurisdiction exposure/penalty all become consistent with
662
+ # the corrected verdict automatically β€” no separate mapping
663
+ # needed for those sections.
664
+ #
665
+ # (b) HARD-NEGATIVE DOWNGRADE β€” only when NO structural pattern
666
+ # matched, and the ML layer flagged a violation, checks a
667
+ # narrow set of known-compliant patterns (currently one:
668
+ # CHN-01, the AAOIFI-permitted actual-loss indemnity formula
669
+ # that was mis-flagged as Gharar/HIGH during pre-meeting
670
+ # testing). A match downgrades the verdict to
671
+ # REVIEW_REQUIRED β€” never silently back to AUTHENTIC. A
672
+ # human still confirms; the system just stops presenting a
673
+ # compliant clause as a confirmed violation.
674
+ #
675
+ # Neither path touches the base model's weights. Both are
676
+ # deterministic, narrow, and fully logged in `reconciliation`
677
+ # below so the audit trail always shows which layer produced the
678
+ # final call.
679
+ # ══════════════════════════════════════════════════════════
680
+ reconciliation = {
681
+ "applied" : False,
682
+ "source" : None,
683
+ "reason" : None,
684
+ "original_ml_violation_class": label,
685
+ "original_ml_severity" : severity,
686
+ "original_ml_confidence" : round(conf, 4),
687
+ }
688
+
689
+ if structural_flags_list:
690
+ top = max(
691
+ structural_flags_list,
692
+ key=lambda f: STRUCTURAL_SEVERITY_RANK.get(f["severity"], 0),
693
+ )
694
+ mapped_pred = STRUCTURAL_PATTERN_TO_PRED.get(top["pattern_id"])
695
+
696
+ if mapped_pred is not None:
697
+ pred = mapped_pred
698
+ label = f"{LABEL_NAMES[mapped_pred]} β€” {top['name']}"
699
+ conf = max(conf, 0.95) # deterministic match β€” high confidence by construction
700
+ tier = "HIGH"
701
+ verdict = "VIOLATION"
702
+ terry = "FLAGGED"
703
+ severity = top["severity"]
704
+
705
+ reconciliation.update({
706
+ "applied": True,
707
+ "source" : "structural_override",
708
+ "reason" : (
709
+ f"Deterministic structural pattern {top['pattern_id']} "
710
+ f"({top['name']}) matched. This overrides the base "
711
+ f"classifier's verdict for this text regardless of "
712
+ f"language β€” structural patterns are validated directly "
713
+ f"against SRB findings and are trusted over a "
714
+ f"probabilistic language-signal miss."
715
+ ),
716
+ "structural_matches": [f["pattern_id"] for f in structural_flags_list],
717
+ })
718
+
719
+ elif verdict == "VIOLATION":
720
+ hard_negative = scan_hard_negatives(text, label, severity)
721
+ if hard_negative is not None:
722
+ severity = "LOW"
723
+ verdict = "REVIEW_REQUIRED"
724
+ terry = "UNCERTAIN"
725
+ needs_review = True
726
+ recommendation = "REVIEW"
727
+ review_reason = f"hard_negative_pattern_match:{hard_negative.pattern_id}"
728
+
729
+ reconciliation.update({
730
+ "applied": True,
731
+ "source" : "hard_negative_downgrade",
732
+ "reason" : (
733
+ f"Base classifier flagged '{reconciliation['original_ml_violation_class']}' "
734
+ f"({reconciliation['original_ml_severity']}), but this text matches known "
735
+ f"AAOIFI-compliant pattern {hard_negative.pattern_id} "
736
+ f"({hard_negative.name}). Downgraded to REVIEW_REQUIRED β€” "
737
+ f"a human confirms, the system does not self-clear to Authentic."
738
+ ),
739
+ "hard_negative_match": hard_negative.pattern_id,
740
+ "hard_negative_aaoifi_standard": hard_negative.aaoifi_standard,
741
+ })
742
+
743
+ # Recompute movement + recommendation now that pred/verdict/severity
744
+ # may have changed above, so downstream fields stay consistent.
745
+ mv = MOVE[pred]
746
+ if verdict in ("AUTHENTIC","PASS"): mv = "deliver_output_to_user"
747
+ if severity=="CRITICAL" and verdict=="VIOLATION": mv = "quarantine_output"
748
+
749
+ if reconciliation["source"] == "structural_override":
750
+ recommendation = "QUARANTINE" if severity == "CRITICAL" else "ESCALATE"
751
+ needs_review = True
752
+
753
  # Phase 2 enrichment
754
  conseq = CONSEQUENCE_TABLE[pred]
755
  altmap = ALTERNATIVE_MAP[pred]
 
760
  if partial and not disclaimer:
761
  disclaimer = "Farsi/Tajik input β€” partial coverage. Scholar review recommended."
762
 
763
+ # NEW (Phase 5): when a structural override fired, use that pattern's
764
+ # own AAOIFI citation and explanation instead of the generic per-class
765
+ # ALTERNATIVE_MAP entry β€” it's more specific to the actual clause than
766
+ # the class-level default.
767
+ compliant_alternative = altmap["primary"]
768
+ structural_fix = altmap["structural_fix"]
769
+ aaoifi_standard = altmap["aaoifi_standard"]
770
+ if reconciliation["source"] == "structural_override":
771
+ aaoifi_standard = top["aaoifi_standard"]
772
+ structural_fix = top["explanation"]
773
+ elif reconciliation["source"] == "hard_negative_downgrade":
774
+ aaoifi_standard = hard_negative.aaoifi_standard
775
+ compliant_alternative = "No change required β€” matches a recognized AAOIFI-compliant pattern"
776
+ structural_fix = hard_negative.explanation
777
+
778
  # Phase 3 jurisdiction
779
  juris_data = JURISDICTION_EXPOSURE.get(pred, {})
780
  jurisdiction_recognized = jurisdiction in juris_data
 
826
  "economic_signal" : econ_signal,
827
  "action_required" : (conseq["action_required"]
828
  if tier=="HIGH" else "human_review_first"),
829
+ "compliant_alternative": compliant_alternative,
830
+ "structural_fix" : structural_fix,
831
+ "aaoifi_standard" : aaoifi_standard,
832
  },
833
 
834
  # Phase 3: Jurisdiction
 
849
  } if pred != 0 else "compliant β€” no exposure",
850
  },
851
 
852
+ # Phase 4: Structural pattern layer
853
  "structural_pattern_flags": {
854
  "layer_type": "rule-based structural detection (Phase 4)",
855
  "note": (
 
862
  "1500-char truncation applied to the ML layer below. Coverage is "
863
  "limited to the patterns currently defined β€” absence of a flag "
864
  "here is NOT confirmation of structural compliance, only that none "
865
+ "of the currently-modeled red-flag patterns matched. As of Phase 5, "
866
+ "a match here also overrides maci_evaluation directly β€” see "
867
+ "`reconciliation` below."
868
  ),
869
  "flags": structural_flags_list,
870
  "flag_count": len(structural_flags_list),
871
  },
872
 
873
+ # NEW β€” Phase 5: Reconciliation layer
874
+ "reconciliation": reconciliation,
875
+
876
  # Overclaiming controls
877
  "overclaiming_controls" : {
878
  "severity_is_confidence_modulated": True,
 
884
  "original_char_count" : original_length,
885
  "structural_layer_overrode_ml_verdict": bool(structural_override_reason),
886
  "structural_override_reason": structural_override_reason,
887
+ "reconciliation_applied": reconciliation["applied"],
888
+ "reconciliation_source": reconciliation["source"],
889
  },
890
 
891
  "boundary_behavior" : {
 
927
  "exposure" : juris_info.get("exposure","unknown"),
928
  "truncated" : was_truncated,
929
  "structural_flags": len(structural_flags_list),
930
+ "reconciliation_applied": reconciliation["applied"],
931
  "audit_hash" : f"sha256:{phash[:16]}...",
932
  },
933
 
 
953
  "(Bay' al-Inah, disguised Riba late-fee, "
954
  "unilateral price variation, risk/ownership "
955
  "decoupling, total liability waiver)",
956
+ "reconciliation_layer": "Phase 5 β€” structural patterns override "
957
+ "maci_evaluation directly; one hard-negative "
958
+ "pattern (CHN-01, actual-loss indemnity) "
959
+ "downgrades known false positives to REVIEW_REQUIRED",
960
  "endpoints" : {
961
  "single" : "POST /api/v1/classify",
962
  "batch" : "POST /api/v1/classify/batch",
 
980
  "ml_f1" : 0.9026,
981
  "rulings" : len(STATE.get("rulings",[])),
982
  "structural_pattern_layer": "active",
983
+ "reconciliation_layer": "active",
984
  }
985
 
986
  @app.post(
 
994
  "Returns the full MACI-0.4 signal packet, including "
995
  "maqasid_enrichment (AAOIFI standard, structural fix, "
996
  "compliant alternative), jurisdiction_overlay, "
997
+ "structural_pattern_flags, reconciliation, and overclaiming_controls."
998
  )
999
  def classify_endpoint(req: ClassifyRequest):
1000
  if "xlmr" not in STATE:
 
1146
  "mechanism_results": results,
1147
 
1148
  "audit_hash" : f"sha256:{audit_hash}",
1149
+ }