Spaces:
Sleeping
Sleeping
| # 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), | |
| } |