Major_Project / explanation.py
riyasuryawanshi746's picture
Explanability and Symbolic part fixed 12th may
af910e9 verified
# explanation.py
# v5.3 — fallback risk_breakdown weights aligned to new fusion config.
from __future__ import annotations
from inference import RULE_FEATURE_DEPS
RULE_EXPLANATIONS = {
"ICA_001": {
"why": "Liability is capped even for gross negligence or wilful misconduct.",
"meaning": "If the other party causes serious harm deliberately, your recovery is still limited by the cap.",
"suggestion": "Carve out gross negligence and wilful misconduct from the liability cap.",
},
"ICA_002": {
"why": "The contract allows one party to terminate without any notice period.",
"meaning": "You could lose the contract relationship immediately with no time to prepare.",
"suggestion": "Negotiate a minimum notice period (typically 30-90 days) before termination takes effect.",
},
"ICA_003": {
"why": "Non-compete clauses exceeding 2 years are void under Indian Contract Act S.27.",
"meaning": "This clause is likely unenforceable in India, yet still creates uncertainty.",
"suggestion": "Limit the non-compete to 12-24 months and narrow its geographic scope.",
},
"ICA_004": {
"why": "Liquidated damages are set without reference to actual loss.",
"meaning": "The amount may be challenged as a penalty clause under S.74 ICA.",
"suggestion": "Link the damages figure to a genuine pre-estimate of foreseeable loss.",
},
"ICA_005": {
"why": "The clause uses gambling, wagering, or betting vocabulary.",
"meaning": "Such agreements are void under Indian Contract Act S.30.",
"suggestion": "Remove or restructure the wagering element of this clause.",
},
"ICA_006": {
"why": "The clause restricts a party from pursuing legal proceedings.",
"meaning": "Agreements that oust court jurisdiction are void under S.28 ICA.",
"suggestion": "Replace with a structured dispute-resolution mechanism (arbitration / mediation).",
},
"ICA_007": {
"why": "An indemnity obligation is paired with uncapped / unlimited liability language.",
"meaning": "You could face open-ended financial exposure for third-party claims.",
"suggestion": "Cap the indemnity at a multiple of contract value and carve out consequential losses.",
},
"ICA_008": {
"why": "The agreement auto-renews without a clearly defined opt-out window.",
"meaning": "You may be locked into another term simply by missing an unstated notice deadline.",
"suggestion": "Add an explicit non-renewal notice window (typically 30-60 days before renewal).",
},
"ICA_009": {
"why": "Arbitration is mandated in a venue that is distant or foreign to one party.",
"meaning": "The cost and inconvenience of distant arbitration can effectively block legitimate claims.",
"suggestion": "Set the seat of arbitration in a neutral, accessible Indian city (e.g. Mumbai, Delhi).",
},
"ICA_010": {
"why": "Exclusivity rights are granted without a defined term, making them open-ended.",
"meaning": "Indefinite restraints of trade are typically void under Indian Contract Act S.27.",
"suggestion": "Fix a clear exclusivity term (e.g. 1-3 years) with defined renewal mechanics.",
},
"ICA_011": {
"why": "One party retains unilateral power to modify prices or fees.",
"meaning": "Discretionary pricing changes can violate the consensus principle under S.62 ICA and CPA 2019.",
"suggestion": "Require mutual agreement or formula-based pricing changes with prior written notice.",
},
"DPDPA_001": {
"why": "Personal data is processed but no data-retention period is specified.",
"meaning": "Non-compliance with DPDPA 2023 S.8(7) - data must not be kept beyond necessity.",
"suggestion": "Add a clause specifying the retention period and a deletion/anonymisation schedule.",
},
"DPDPA_002": {
"why": "All IP including pre-existing background IP is assigned to the client.",
"meaning": "You permanently lose rights to work created before this engagement.",
"suggestion": "Explicitly exclude pre-existing IP from the assignment scope.",
},
"DPDPA_003": {
"why": "Sensitive personal data is processed without a consent mechanism.",
"meaning": "Violates DPDPA 2023 S.6 which mandates explicit, informed consent.",
"suggestion": "Add a consent clause with opt-in mechanism and purpose limitation.",
},
"DPDPA_004": {
"why": "Personal data is processed but no breach-notification obligation exists.",
"meaning": "DPDPA 2023 S.8(6) requires timely notification of data breaches.",
"suggestion": "Include a breach-notification clause with a 72-hour reporting window.",
},
"ITA_001": {
"why": "Digital data is handled but no cybersecurity obligations are specified.",
"meaning": "Exposes the party to liability under IT Act 2000 S.43A.",
"suggestion": "Add a security-measures clause referencing ISO 27001 or equivalent standards.",
},
"CPA_001": {
"why": "The contract is with a consumer and contains a one-sided or unfair term.",
"meaning": "Such terms may be declared void under Consumer Protection Act 2019 S.2(46).",
"suggestion": "Rebalance the clause to ensure mutual obligations and remove absolute discretion.",
},
}
RISK_CONTEXT = {
"Low": "This clause appears relatively standard with minimal legal exposure.",
"Medium": "This clause contains terms that warrant careful review before signing.",
"High": "This clause poses significant legal or financial risk and should be renegotiated.",
}
CATEGORY_CONTEXT = {
"financial": "financial exposure or uncapped monetary liability",
"enforceability": "enforceability concerns - the clause may be void or unenforceable in India",
"ip": "intellectual property rights that may be unfairly transferred",
"compliance": "regulatory non-compliance with Indian data-protection law",
"structural": "structural or procedural terms with lower inherent risk",
"ambiguity": "ambiguous language that could be interpreted against your interests",
}
def _evidence_for_rule(rule_id: str, evidence: dict) -> list[dict]:
deps = RULE_FEATURE_DEPS.get(rule_id, [])
snippets: list[dict] = []
for feat in deps:
for hit in (evidence.get(feat) or []):
snippets.append({
"feature": feat,
"phrase": hit.get("phrase", ""),
"span": hit.get("span", [0, 0]),
})
return snippets[:4]
def _flat_evidence(evidence: dict) -> list[dict]:
out = []
for feat, hits in (evidence or {}).items():
for h in hits:
out.append({
"feature": feat,
"matched_phrase": h.get("phrase", ""),
"span": h.get("span", [0, 0]),
"label": h.get("label", ""),
})
return out
def _format_score_breakdown_text(breakdown: dict | None, fused: float) -> str:
if not breakdown:
return f"Final Score = {fused:.2f} (model-only)."
w = breakdown.get("weights", {})
nrm = breakdown.get("neural_score", 0.0)
sym = breakdown.get("symbolic_score", 0.0)
fin = breakdown.get("final", fused)
return (
f"Final Score = {fin:.2f} "
f"(Neural {nrm:.2f} × {w.get('neural', 0):.2f} + "
f"Symbolic {sym:.2f} × {w.get('symbolic', 0):.2f})"
)
def generate_explanation(text: str, result: dict) -> dict:
level_raw = result.get("risk_level_raw", "Low")
triggered = result.get("triggered_rules", [])
top_cats = result.get("top_risk_cats", [])
risk_score = result.get("risk_score", 0.0)
evidence = result.get("evidence", {}) or {}
breakdown = result.get("score_breakdown")
confidence = result.get("confidence") or {}
primary_cat = top_cats[0][0] if top_cats else "structural"
cat_desc = CATEGORY_CONTEXT.get(primary_cat, "legal concerns")
overview = (
f"{RISK_CONTEXT.get(level_raw, RISK_CONTEXT['Low'])} "
f"The primary concern is {cat_desc} (fused risk score: {risk_score:.2f})."
)
rule_details = []
for rule in triggered:
rid = rule.get("rule_id", "")
tmpl = RULE_EXPLANATIONS.get(rid, {})
rule_details.append({
"rule_id": rid,
"name": rule.get("name", rid),
"reference": rule.get("reference", ""),
"penalty": rule.get("penalty", 0),
"category": rule.get("category", ""),
"why": tmpl.get("why", "This clause triggered a legal risk flag."),
"meaning": tmpl.get("meaning", "Review the clause carefully before signing."),
"suggestion":tmpl.get("suggestion", "Seek legal advice on this provision."),
"evidence": _evidence_for_rule(rid, evidence),
})
general_tip = ""
if not triggered:
if level_raw == "Low":
general_tip = "No specific Indian-law violations detected. Standard review recommended."
elif level_raw == "Medium":
general_tip = ("The neural model flags moderate risk. Review clause language "
"around obligations, duration, and liability.")
else:
general_tip = ("High neural risk score despite no specific rule triggers. "
"The clause may contain broad or one-sided language - seek legal review.")
# v5.3: fallback weights updated to new neural-dominant config
risk_breakdown = breakdown or {
"neural_score": result.get("neural_score", 0.0),
"symbolic_score": result.get("symbolic_score", 0.0),
"weights": {"neural": 0.75, "symbolic": 0.25},
"raw_fused": risk_score,
"floor_applied": False,
"final": risk_score,
"formula": f"(0.75 × {result.get('neural_score', 0):.3f}) + "
f"(0.25 × {result.get('symbolic_score', 0):.3f}) "
f"= {risk_score:.3f}",
}
return {
"overview": overview,
"rules": rule_details,
"general_tip": general_tip,
"score_breakdown": breakdown,
"confidence": confidence,
"risk_breakdown": risk_breakdown,
"evidence": _flat_evidence(evidence),
"confidence_level": confidence.get("level", "Medium"),
"natural_language_summary": "",
"score_breakdown_text": _format_score_breakdown_text(breakdown, risk_score),
}