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