Spaces:
Running
Running
| """ | |
| ClauseGuard β Clause Redlining Engine v1.0 | |
| βββββββββββββββββββββββββββββββββββββββββββ | |
| 3-Tier Hybrid Architecture: | |
| Tier 1 β Template lookup (instant, zero hallucination risk) | |
| Tier 2 β RAG retrieval from clause corpus (find fairer precedents) | |
| Tier 3 β LLM refinement (adapt template using retrieved precedents) | |
| Anti-hallucination guardrails: | |
| β’ Template anchor: LLM can only refine, not generate from scratch | |
| β’ RAG grounding: Retrieved precedents constrain the output space | |
| β’ Disclaimer: "Not legal advice. Consult an attorney before executing." | |
| β’ Legal citation: Prompt requires LLM to cite the consumer protection standard applied | |
| """ | |
| import os | |
| import re | |
| from collections import defaultdict | |
| # ββ HF Inference Client (soft-fail) βββββββββββββββββββββββββββββββββ | |
| _HAS_INFERENCE = False | |
| try: | |
| from huggingface_hub import InferenceClient | |
| _HAS_INFERENCE = True | |
| except ImportError: | |
| pass | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TIER 1: TEMPLATE LIBRARY (18+ clause types) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Based on FTC guidelines, EU Directive 93/13, and CFPB guidance. | |
| SAFE_ALTERNATIVES = { | |
| # ββ CRITICAL Risk Clauses ββββββββββββββββββββββββββββββββββββββ | |
| "Uncapped Liability": { | |
| "risky_pattern": "Total liability shall not exceed $1 / unlimited liability exposure", | |
| "safe_alternative": ( | |
| "Provider's aggregate liability under this Agreement shall not exceed the total " | |
| "fees paid by the Customer in the twelve (12) months preceding the claim. " | |
| "This limitation shall not apply to: (a) gross negligence or willful misconduct, " | |
| "(b) breach of confidentiality obligations, (c) intellectual property indemnification " | |
| "obligations, or (d) violations of applicable law." | |
| ), | |
| "legal_basis": "UCC Β§ 2-719; Restatement (Second) of Contracts Β§ 356", | |
| "consumer_standard": "FTC guidelines on unconscionable contract terms", | |
| "risk_level": "CRITICAL", | |
| }, | |
| "Arbitration": { | |
| "risky_pattern": "All disputes via binding arbitration / class action waiver", | |
| "safe_alternative": ( | |
| "Disputes involving claims under [Dollar Amount] shall be resolved in small claims " | |
| "court in the consumer's jurisdiction of residence. For other disputes, either party " | |
| "may elect binding arbitration under [AAA/JAMS] rules. The consumer may opt out of " | |
| "arbitration by providing written notice within thirty (30) days of accepting these " | |
| "terms. Each party bears its own arbitration costs; the prevailing party may recover " | |
| "reasonable attorney's fees." | |
| ), | |
| "legal_basis": "Federal Arbitration Act Β§ 2; AT&T Mobility v. Concepcion, 563 U.S. 333 (2011)", | |
| "consumer_standard": "CFPB Arbitration Rule guidance; EU Directive 93/13/EEC Art. 3", | |
| "risk_level": "CRITICAL", | |
| }, | |
| "IP Ownership Assignment": { | |
| "risky_pattern": "All IP rights assigned to company / work-for-hire everything", | |
| "safe_alternative": ( | |
| "Intellectual property created by the Receiving Party specifically in performance of " | |
| "this Agreement ('Work Product IP') shall be assigned to the Disclosing Party. " | |
| "Pre-existing IP and general knowledge, skills, and experience of the Receiving Party " | |
| "remain the Receiving Party's property. The Disclosing Party grants the Receiving Party " | |
| "a non-exclusive, perpetual license to use Work Product IP for internal portfolio and " | |
| "reference purposes." | |
| ), | |
| "legal_basis": "17 U.S.C. Β§ 101 (work for hire); Copyright Act Β§ 201(b)", | |
| "consumer_standard": "Standard IP assignment with carve-outs for pre-existing IP", | |
| "risk_level": "CRITICAL", | |
| }, | |
| "Termination for Convenience": { | |
| "risky_pattern": "Terminate at any time without notice", | |
| "safe_alternative": ( | |
| "Either party may terminate this Agreement for convenience upon thirty (30) days' " | |
| "prior written notice. Immediate termination is permitted only for material breach " | |
| "that remains uncured after a ten (10) day cure period following written notice " | |
| "specifying the breach. Upon termination: (a) all outstanding fees become due, " | |
| "(b) each party shall return or destroy confidential information within fifteen (15) " | |
| "business days, and (c) licenses granted hereunder shall terminate except as " | |
| "expressly stated to survive." | |
| ), | |
| "legal_basis": "Restatement (Second) of Contracts Β§ 237; UCC Β§ 2-309", | |
| "consumer_standard": "FTC: adequate notice period required for service termination", | |
| "risk_level": "CRITICAL", | |
| }, | |
| "Limitation of liability": { | |
| "risky_pattern": "Company not liable for any damages / complete disclaimer", | |
| "safe_alternative": ( | |
| "Neither party shall be liable for indirect, incidental, special, or consequential " | |
| "damages, EXCEPT in cases of: (a) gross negligence or willful misconduct, " | |
| "(b) breach of confidentiality, (c) data breach involving personal information, or " | |
| "(d) intellectual property infringement. Direct damages are limited to fees paid " | |
| "in the prior twelve (12) months. Nothing in this Agreement limits liability for " | |
| "death or personal injury caused by negligence." | |
| ), | |
| "legal_basis": "UCC Β§ 2-719(3); EU Directive 93/13/EEC Annex (a)", | |
| "consumer_standard": "Cannot exclude liability for death/personal injury (EU/UK law)", | |
| "risk_level": "CRITICAL", | |
| }, | |
| "Unilateral termination": { | |
| "risky_pattern": "Company can terminate account at any time without reason", | |
| "safe_alternative": ( | |
| "The Provider may suspend or terminate the User's account for: (a) material breach " | |
| "of these Terms, (b) non-payment after ten (10) days' notice, (c) illegal activity, " | |
| "or (d) extended inactivity exceeding twelve (12) months. The Provider shall provide " | |
| "at least thirty (30) days' written notice before termination, except in cases of " | |
| "illegal activity. Upon termination, the User shall have thirty (30) days to export " | |
| "their data." | |
| ), | |
| "legal_basis": "EU Directive 2019/770 (Digital Content); CFPB guidance", | |
| "consumer_standard": "Right to export data upon termination; adequate notice period", | |
| "risk_level": "CRITICAL", | |
| }, | |
| "Liquidated Damages": { | |
| "risky_pattern": "Pre-determined damages far exceeding actual harm", | |
| "safe_alternative": ( | |
| "In the event of breach, the non-breaching party shall be entitled to liquidated " | |
| "damages in the amount of [specific reasonable amount], which the parties agree " | |
| "represents a reasonable estimate of anticipated harm. This liquidated damages " | |
| "provision shall not apply if actual damages are readily ascertainable, in which " | |
| "case the non-breaching party may recover actual damages proven." | |
| ), | |
| "legal_basis": "Restatement (Second) of Contracts Β§ 356; UCC Β§ 2-718", | |
| "consumer_standard": "Liquidated damages must be reasonable estimate, not penalty", | |
| "risk_level": "CRITICAL", | |
| }, | |
| # ββ HIGH Risk Clauses ββββββββββββββββββββββββββββββββββββββββββ | |
| "Unilateral change": { | |
| "risky_pattern": "We may modify terms at any time without notice", | |
| "safe_alternative": ( | |
| "Material changes to these Terms require thirty (30) days' advance written notice " | |
| "to the User via email and in-app notification. The User has the right to terminate " | |
| "without penalty within the notice period if they do not accept the changes. " | |
| "Non-material changes (e.g., formatting, clarifications) may be made without notice." | |
| ), | |
| "legal_basis": "EU Directive 93/13/EEC Art. 3; Restatement (Second) Β§ 89", | |
| "consumer_standard": "FTC: material changes require notice and right to reject", | |
| "risk_level": "HIGH", | |
| }, | |
| "Content removal": { | |
| "risky_pattern": "Company can delete content at sole discretion without notice", | |
| "safe_alternative": ( | |
| "Content may be removed only for violation of these Terms of Service, applicable law, " | |
| "or valid legal process. The Provider shall provide prior notice specifying the reason " | |
| "for removal (except where legally prohibited). The User has the right to appeal " | |
| "within fourteen (14) days. Removed content shall be preserved for thirty (30) days " | |
| "to allow for appeal resolution." | |
| ), | |
| "legal_basis": "EU Digital Services Act Art. 17; First Amendment considerations", | |
| "consumer_standard": "Due process: notice, reason, and right to appeal", | |
| "risk_level": "HIGH", | |
| }, | |
| "Non-Compete": { | |
| "risky_pattern": "Broad non-compete with no time/geography limits", | |
| "safe_alternative": ( | |
| "During the term of this Agreement and for a period of [6-12] months thereafter, " | |
| "the Receiving Party shall not directly compete with the Disclosing Party in " | |
| "[specific market/geography]. This restriction applies only to [specific business " | |
| "activities] and does not prevent general employment in the industry. The Disclosing " | |
| "Party shall provide [garden leave pay / consideration] during the restricted period." | |
| ), | |
| "legal_basis": "Restatement (Second) of Contracts Β§ 188; FTC Non-Compete Rule (2024)", | |
| "consumer_standard": "Reasonable scope, duration, geography; adequate consideration", | |
| "risk_level": "HIGH", | |
| }, | |
| "Exclusivity": { | |
| "risky_pattern": "Exclusive dealing with no time limit or exit clause", | |
| "safe_alternative": ( | |
| "The exclusivity arrangement shall apply for an initial term of [12-24] months, " | |
| "after which either party may convert to non-exclusive upon sixty (60) days' notice. " | |
| "Exclusivity is limited to [specific product/service category] and [specific " | |
| "geographic area]. Performance benchmarks shall be reviewed quarterly; failure to " | |
| "meet agreed minimums allows termination of exclusivity." | |
| ), | |
| "legal_basis": "Sherman Act Β§ 1; EU Competition Law Art. 101 TFEU", | |
| "consumer_standard": "Time-limited, scope-limited, with performance exit clause", | |
| "risk_level": "HIGH", | |
| }, | |
| "Anti-Assignment": { | |
| "risky_pattern": "Complete prohibition on assignment without consent", | |
| "safe_alternative": ( | |
| "Neither party may assign this Agreement without the prior written consent of the " | |
| "other party, which shall not be unreasonably withheld, conditioned, or delayed. " | |
| "Notwithstanding the foregoing, either party may assign this Agreement without " | |
| "consent in connection with a merger, acquisition, or sale of substantially all " | |
| "of its assets, provided the assignee assumes all obligations hereunder." | |
| ), | |
| "legal_basis": "UCC Β§ 2-210; Restatement (Second) of Contracts Β§ 317", | |
| "consumer_standard": "Consent not to be unreasonably withheld; M&A carve-out", | |
| "risk_level": "HIGH", | |
| }, | |
| # ββ MEDIUM Risk Clauses ββββββββββββββββββββββββββββββββββββββββ | |
| "Jurisdiction": { | |
| "risky_pattern": "Exclusive jurisdiction in distant/foreign state", | |
| "safe_alternative": ( | |
| "The Consumer may bring claims in their jurisdiction of residence or the Provider's " | |
| "principal place of business. Small claims actions may be brought in any court of " | |
| "competent jurisdiction. For commercial contracts: disputes shall be resolved in " | |
| "[mutually agreed location] or the defendant's principal place of business." | |
| ), | |
| "legal_basis": "EU Regulation 1215/2012 (Brussels I); CJEU C-585/08", | |
| "consumer_standard": "Consumer may sue in home jurisdiction (EU Directive 93/13)", | |
| "risk_level": "MEDIUM", | |
| }, | |
| "Choice of law": { | |
| "risky_pattern": "Governed by laws of a jurisdiction that disadvantages consumer", | |
| "safe_alternative": ( | |
| "This Agreement shall be governed by the laws of [State/Country]. Notwithstanding " | |
| "the foregoing, nothing in this choice of law provision shall deprive the Consumer " | |
| "of the protection afforded by mandatory provisions of the law of the Consumer's " | |
| "habitual residence." | |
| ), | |
| "legal_basis": "EU Regulation 593/2008 (Rome I) Art. 6; UCC Β§ 1-301", | |
| "consumer_standard": "Cannot override mandatory consumer protection of home jurisdiction", | |
| "risk_level": "MEDIUM", | |
| }, | |
| "Contract by using": { | |
| "risky_pattern": "Bound to contract by merely using the service (browsewrap)", | |
| "safe_alternative": ( | |
| "By creating an account, the User acknowledges they have read, understood, and agree " | |
| "to be bound by these Terms. The User must affirmatively accept these Terms via " | |
| "checkbox or click-through before account creation. Continued use after material " | |
| "changes requires re-acceptance." | |
| ), | |
| "legal_basis": "Specht v. Netscape, 306 F.3d 17 (2d Cir. 2002)", | |
| "consumer_standard": "Clickwrap > browsewrap; affirmative acceptance required", | |
| "risk_level": "MEDIUM", | |
| }, | |
| # ββ Additional Common Clauses ββββββββββββββββββββββββββββββββββ | |
| "Auto-Renewal": { | |
| "risky_pattern": "Auto-renews silently without notice", | |
| "safe_alternative": ( | |
| "This Agreement shall automatically renew for successive [term] periods unless " | |
| "either party provides written notice of non-renewal at least thirty (30) days " | |
| "before the end of the then-current term. The Provider shall send a reminder " | |
| "notice thirty (30) to sixty (60) days before renewal. The Consumer may cancel " | |
| "within fifteen (15) days of renewal for a pro-rated refund." | |
| ), | |
| "legal_basis": "California Auto-Renewal Law (ARL) Bus. & Prof. Code Β§ 17600; FTC Negative Option Rule", | |
| "consumer_standard": "Reminder notice required; easy cancellation; pro-rated refund", | |
| "risk_level": "HIGH", | |
| }, | |
| "Indemnification": { | |
| "risky_pattern": "User indemnifies company for all claims without limit", | |
| "safe_alternative": ( | |
| "Each party shall indemnify, defend, and hold harmless the other party from " | |
| "third-party claims arising from: (a) the indemnifying party's breach of this " | |
| "Agreement, (b) the indemnifying party's negligence or willful misconduct, or " | |
| "(c) the indemnifying party's violation of applicable law. The User's indemnification " | |
| "obligation is limited to claims arising from the User's own negligence or " | |
| "intentional acts. The maximum indemnification obligation shall not exceed [amount]." | |
| ), | |
| "legal_basis": "Restatement (Second) of Contracts Β§ 345; UCC Β§ 2-607", | |
| "consumer_standard": "Mutual indemnification; limited to own acts; capped", | |
| "risk_level": "HIGH", | |
| }, | |
| "Confidentiality": { | |
| "risky_pattern": "Overly broad confidentiality with no exceptions or time limit", | |
| "safe_alternative": ( | |
| "Each party agrees to maintain the confidentiality of the other's Confidential " | |
| "Information for a period of [3-5] years from disclosure. Confidential Information " | |
| "excludes: (a) publicly available information, (b) independently developed " | |
| "information, (c) information received from a third party without restriction, " | |
| "(d) information required to be disclosed by law or court order (with prompt notice " | |
| "to the disclosing party)." | |
| ), | |
| "legal_basis": "Restatement (Third) of Unfair Competition Β§ 39-45", | |
| "consumer_standard": "Time-limited; standard exceptions; required disclosure carve-out", | |
| "risk_level": "MEDIUM", | |
| }, | |
| } | |
| # Mapping from CUAD/unfair labels to our template keys | |
| _LABEL_TO_TEMPLATE = { | |
| "Uncapped Liability": "Uncapped Liability", | |
| "Arbitration": "Arbitration", | |
| "IP Ownership Assignment": "IP Ownership Assignment", | |
| "Termination for Convenience": "Termination for Convenience", | |
| "Limitation of liability": "Limitation of liability", | |
| "Unilateral termination": "Unilateral termination", | |
| "Liquidated Damages": "Liquidated Damages", | |
| "Unilateral change": "Unilateral change", | |
| "Content removal": "Content removal", | |
| "Non-Compete": "Non-Compete", | |
| "Exclusivity": "Exclusivity", | |
| "Anti-Assignment": "Anti-Assignment", | |
| "Jurisdiction": "Jurisdiction", | |
| "Choice of law": "Choice of law", | |
| "Contract by using": "Contract by using", | |
| "Cap on Liability": "Limitation of liability", # Similar enough | |
| "No-Solicit of Customers": "Non-Compete", # Use non-compete template | |
| "No-Solicit of Employees": "Non-Compete", | |
| "Non-Disparagement": "Confidentiality", # Similar restrictive clause | |
| } | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TIER 2: RAG RETRIEVAL (find fairer precedent clauses) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _find_similar_templates(clause_label, clause_text): | |
| """ | |
| Find the most relevant safe alternative template(s) for a given clause. | |
| Returns list of matching templates. | |
| """ | |
| matches = [] | |
| # Direct label match | |
| template_key = _LABEL_TO_TEMPLATE.get(clause_label) | |
| if template_key and template_key in SAFE_ALTERNATIVES: | |
| matches.append((template_key, SAFE_ALTERNATIVES[template_key], 1.0)) | |
| # Also do keyword matching for clauses that might not have exact label matches | |
| clause_lower = clause_text.lower() | |
| keyword_map = { | |
| "Uncapped Liability": ["unlimited liability", "uncapped", "no limit on liability"], | |
| "Arbitration": ["arbitration", "arbitrate", "waive right to court", "class action waiver"], | |
| "Termination for Convenience": ["terminate at any time", "terminate without cause", "terminate without notice"], | |
| "Limitation of liability": ["not liable", "limitation of liability", "in no event", "disclaim"], | |
| "Unilateral change": ["modify at any time", "sole discretion", "change terms", "without notice"], | |
| "Content removal": ["remove content", "delete content", "remove at sole discretion"], | |
| "Auto-Renewal": ["auto-renew", "automatically renew", "automatic renewal"], | |
| "Indemnification": ["indemnif", "hold harmless"], | |
| } | |
| for key, keywords in keyword_map.items(): | |
| if key in SAFE_ALTERNATIVES: | |
| for kw in keywords: | |
| if kw in clause_lower: | |
| # Avoid duplicates | |
| if not any(m[0] == key for m in matches): | |
| matches.append((key, SAFE_ALTERNATIVES[key], 0.7)) | |
| break | |
| return matches | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TIER 3: LLM REFINEMENT | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _LLM_MODEL = "Qwen/Qwen2.5-7B-Instruct" | |
| def _refine_with_llm(original_clause, template, clause_label): | |
| """ | |
| Use LLM to adapt the template to the specific clause context. | |
| The LLM refines β it does NOT generate from scratch (anti-hallucination). | |
| """ | |
| if not _HAS_INFERENCE: | |
| return None | |
| try: | |
| token = os.environ.get("HF_TOKEN", "") | |
| client = InferenceClient( | |
| provider="hf-inference", | |
| api_key=token if token else None, | |
| ) | |
| prompt = f"""You are a legal contract redlining assistant. Your task is to adapt a safe clause template to fit the specific context of an original risky clause. | |
| RULES: | |
| 1. You MUST use the provided template as your base β do NOT generate clauses from scratch. | |
| 2. Preserve the legal protections in the template. | |
| 3. Adapt specific details (parties, amounts, timeframes) from the original clause. | |
| 4. Keep the same legal standard cited in the template. | |
| 5. Output ONLY the refined clause text, nothing else. | |
| 6. The refined clause should be immediately usable in a contract. | |
| ORIGINAL RISKY CLAUSE: | |
| {original_clause[:500]} | |
| CLAUSE TYPE: {clause_label} | |
| SAFE TEMPLATE: | |
| {template['safe_alternative']} | |
| LEGAL BASIS: {template['legal_basis']} | |
| Write the refined safer clause (adapt the template to this specific contract's context):""" | |
| response = client.chat_completion( | |
| model=_LLM_MODEL, | |
| messages=[ | |
| {"role": "system", "content": "You are a legal contract redlining expert. Output ONLY the refined clause text."}, | |
| {"role": "user", "content": prompt}, | |
| ], | |
| max_tokens=512, | |
| temperature=0.2, | |
| ) | |
| refined = response.choices[0].message.content.strip() | |
| # Sanity check: refined should be substantial | |
| if len(refined) < 50: | |
| return None | |
| return refined | |
| except Exception as e: | |
| print(f"[ClauseGuard Redline] LLM refinement error: {e}") | |
| return None | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # FIX v4.3: Keyword validation β ensure original clause matches the label | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _LABEL_KEYWORDS = { | |
| "Limitation of liability": ["liable", "liability", "damages", "limitation of liability", "in no event"], | |
| "Uncapped Liability": ["uncapped", "unlimited", "no limit", "no cap"], | |
| "Governing Law": ["governed by", "governing law", "jurisdiction", "laws of"], | |
| "Termination for Convenience": ["terminat", "cancel", "convenience", "without cause"], | |
| "Non-Compete": ["non-compete", "not compete", "competition restriction"], | |
| "No-Solicit of Employees": ["solicit", "recruit", "induce", "encourage", "employee"], | |
| "No-Solicit of Customers": ["solicit", "customer", "client", "divert"], | |
| "Non-Disparagement": ["disparag", "defam", "negative", "derogatory"], | |
| "Arbitration": ["arbitrat", "binding arbitration", "waive", "class action"], | |
| "IP Ownership Assignment": ["intellectual property", "ip", "assign", "work for hire", "ownership"], | |
| "Indemnification": ["indemnif", "hold harmless", "defend"], | |
| "Confidentiality": ["confidential", "non-disclosure", "nda"], | |
| "Exclusivity": ["exclusive", "exclusivity"], | |
| "Anti-Assignment": ["assign", "transfer", "without consent"], | |
| "Content removal": ["remove", "delete", "content"], | |
| "Unilateral change": ["modify", "change", "amend", "sole discretion"], | |
| "Unilateral termination": ["terminat", "suspend", "at any time"], | |
| "Liquidated Damages": ["liquidated", "pre-determined", "stipulated"], | |
| "Choice of law": ["governed by", "laws of", "choice of law"], | |
| "Jurisdiction": ["jurisdiction", "courts of", "exclusive jurisdiction"], | |
| "Contract by using": ["by using", "continued use", "acceptance"], | |
| } | |
| # FIX v4.3.1: Exclusion keywords β if ANY of these appear, the clause is rejected for this label. | |
| # Catches chunks that span two sections (e.g., Β§12.5 Waiver + Β§12.6 Non-Solicitation merged into one chunk). | |
| _LABEL_EXCLUDE_KEYWORDS = { | |
| "No-Solicit of Employees": ["waiver of", "waive any", "waives the right", "failure to enforce"], | |
| "No-Solicit of Customers": ["waiver of", "waive any", "waives the right", "failure to enforce"], | |
| "Non-Disparagement": ["arbitrat", "aaa", "jams", "class action", "waives any right to participate"], | |
| } | |
| def _validate_clause_match(label, clause_text): | |
| """FIX v4.3.1: Validate clause matches label β checks BOTH required AND excluded keywords.""" | |
| text_lower = clause_text.lower() | |
| # Check exclusions first β hard reject | |
| exclusions = _LABEL_EXCLUDE_KEYWORDS.get(label, []) | |
| if exclusions and any(kw in text_lower for kw in exclusions): | |
| return False | |
| # Check required keywords | |
| keywords = _LABEL_KEYWORDS.get(label, []) | |
| if not keywords: | |
| return True | |
| return any(kw in text_lower for kw in keywords) | |
| def generate_redlines(analysis_result, use_llm=True): | |
| """ | |
| Generate redline suggestions for all flagged clauses in the analysis. | |
| FIX v4.3: | |
| - Validates original clause matches label keywords before showing | |
| - Deduplicates by suggested text (catches template mapping bugs) | |
| - Picks the BEST clause for each label (highest confidence + keyword match) | |
| """ | |
| if analysis_result is None: | |
| return [] | |
| clauses = analysis_result.get("clauses", []) | |
| if not clauses: | |
| return [] | |
| # FIX v4.3: Group clauses by label and pick the best match for each | |
| label_clauses = {} | |
| for clause in clauses: | |
| label = clause.get("label", "") | |
| risk = clause.get("risk", "LOW") | |
| text = clause.get("text", "") | |
| confidence = clause.get("confidence", 0) or 0 | |
| if risk == "LOW": | |
| continue | |
| # Validate that the clause text actually matches the label | |
| if not _validate_clause_match(label, text): | |
| continue | |
| # Keep the highest-confidence match for each label | |
| if label not in label_clauses or confidence > (label_clauses[label].get("confidence", 0) or 0): | |
| label_clauses[label] = clause | |
| redlines = [] | |
| seen_alternatives = set() # FIX v4.3: Dedup by suggested text | |
| # Sort by risk level: CRITICAL first | |
| risk_order = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3} | |
| sorted_labels = sorted( | |
| label_clauses.keys(), | |
| key=lambda l: risk_order.get(label_clauses[l].get("risk", "LOW"), 3) | |
| ) | |
| for label in sorted_labels: | |
| clause = label_clauses[label] | |
| risk = clause.get("risk", "LOW") | |
| text = clause.get("text", "") | |
| # Find matching templates (Tier 1 + Tier 2) | |
| matches = _find_similar_templates(label, text) | |
| if not matches: | |
| continue | |
| best_key, best_template, score = matches[0] | |
| # FIX v4.3: Dedup β skip if this template's alternative was already used | |
| alt_fingerprint = best_template["safe_alternative"][:120] | |
| if alt_fingerprint in seen_alternatives: | |
| continue | |
| seen_alternatives.add(alt_fingerprint) | |
| # Tier 3: Try LLM refinement if enabled | |
| refined_text = None | |
| tier = "template" | |
| if use_llm and risk in ("CRITICAL", "HIGH"): | |
| refined_text = _refine_with_llm(text, best_template, label) | |
| if refined_text: | |
| tier = "llm_refined" | |
| redlines.append({ | |
| "original_text": text[:500], | |
| "clause_label": label, | |
| "risk_level": risk, | |
| "safe_alternative": refined_text or best_template["safe_alternative"], | |
| "template_alternative": best_template["safe_alternative"], | |
| "legal_basis": best_template["legal_basis"], | |
| "consumer_standard": best_template["consumer_standard"], | |
| "tier": tier, | |
| }) | |
| return redlines | |
| def render_redlines_html(redlines): | |
| """Render redline suggestions as HTML for Gradio.""" | |
| if not redlines: | |
| return '''<div style="padding:24px;text-align:center;color:#6b7280;font-family:system-ui,sans-serif;"> | |
| <p style="font-size:16px;">π No redline suggestions available.</p> | |
| <p style="font-size:13px;">Analyze a contract first β redlining suggestions will appear for risky clauses.</p> | |
| </div>''' | |
| risk_styles = { | |
| "CRITICAL": ("#dc2626", "#fef2f2", "β οΈ"), | |
| "HIGH": ("#ea580c", "#fff7ed", "β‘"), | |
| "MEDIUM": ("#ca8a04", "#fefce8", "π"), | |
| "LOW": ("#16a34a", "#f0fdf4", "β"), | |
| } | |
| html = '<div style="font-family:system-ui,sans-serif;">' | |
| # Summary header | |
| crit = sum(1 for r in redlines if r["risk_level"] == "CRITICAL") | |
| high = sum(1 for r in redlines if r["risk_level"] == "HIGH") | |
| med = sum(1 for r in redlines if r["risk_level"] == "MEDIUM") | |
| llm_count = sum(1 for r in redlines if r["tier"] == "llm_refined") | |
| html += f''' | |
| <div style="padding:16px;background:linear-gradient(135deg,#eff6ff,#f0fdf4);border-radius:12px;margin-bottom:16px;border:1px solid #e5e7eb;"> | |
| <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;"> | |
| <span style="font-size:24px;">βοΈ</span> | |
| <h2 style="margin:0;font-size:18px;color:#1f2937;">Clause Redlining Suggestions</h2> | |
| </div> | |
| <p style="font-size:13px;color:#6b7280;margin:0;"> | |
| {len(redlines)} suggestions: {crit} Critical Β· {high} High Β· {med} Medium | |
| {f" Β· {llm_count} LLM-refined" if llm_count else ""} | |
| </p> | |
| </div> | |
| ''' | |
| for i, redline in enumerate(redlines): | |
| border_color, bg_color, icon = risk_styles.get( | |
| redline["risk_level"], ("#6b7280", "#f9fafb", "β’") | |
| ) | |
| tier_badge = ( | |
| '<span style="font-size:10px;background:#eff6ff;color:#3b82f6;padding:2px 8px;border-radius:4px;">π€ LLM Refined</span>' | |
| if redline["tier"] == "llm_refined" | |
| else '<span style="font-size:10px;background:#f0fdf4;color:#16a34a;padding:2px 8px;border-radius:4px;">π Template</span>' | |
| ) | |
| original_preview = redline["original_text"][:200].replace("<", "<").replace(">", ">") | |
| safe_text = redline["safe_alternative"].replace("<", "<").replace(">", ">") | |
| html += f''' | |
| <div style="border:1px solid #e5e7eb;border-left:4px solid {border_color};border-radius:8px;margin-bottom:12px;overflow:hidden;"> | |
| <!-- Header --> | |
| <div style="padding:12px 16px;background:{bg_color};border-bottom:1px solid #e5e7eb;"> | |
| <div style="display:flex;align-items:center;justify-content:space-between;"> | |
| <div style="display:flex;align-items:center;gap:8px;"> | |
| <span style="font-size:16px;">{icon}</span> | |
| <span style="font-size:14px;font-weight:600;color:{border_color};">{redline["clause_label"]}</span> | |
| <span style="font-size:11px;color:{border_color};text-transform:uppercase;font-weight:600;">{redline["risk_level"]}</span> | |
| </div> | |
| {tier_badge} | |
| </div> | |
| </div> | |
| <!-- Body --> | |
| <div style="padding:16px;"> | |
| <!-- Original (risky) --> | |
| <div style="margin-bottom:12px;"> | |
| <div style="font-size:11px;font-weight:600;color:#991b1b;text-transform:uppercase;margin-bottom:4px;">β Original (Risky)</div> | |
| <div style="background:#fef2f2;border:1px solid #fecaca;border-radius:6px;padding:10px;font-size:12px;color:#991b1b;line-height:1.6;"> | |
| <del>{original_preview}{"..." if len(redline["original_text"]) > 200 else ""}</del> | |
| </div> | |
| </div> | |
| <!-- Suggested (safe) --> | |
| <div style="margin-bottom:12px;"> | |
| <div style="font-size:11px;font-weight:600;color:#166534;text-transform:uppercase;margin-bottom:4px;">β Suggested Alternative</div> | |
| <div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:6px;padding:10px;font-size:12px;color:#166534;line-height:1.6;"> | |
| {safe_text} | |
| </div> | |
| </div> | |
| <!-- Legal basis --> | |
| <div style="display:flex;gap:12px;flex-wrap:wrap;"> | |
| <div style="flex:1;min-width:200px;"> | |
| <div style="font-size:10px;font-weight:600;color:#6b7280;text-transform:uppercase;margin-bottom:2px;">π Legal Basis</div> | |
| <div style="font-size:11px;color:#4b5563;">{redline["legal_basis"]}</div> | |
| </div> | |
| <div style="flex:1;min-width:200px;"> | |
| <div style="font-size:10px;font-weight:600;color:#6b7280;text-transform:uppercase;margin-bottom:2px;">π‘οΈ Consumer Standard</div> | |
| <div style="font-size:11px;color:#4b5563;">{redline["consumer_standard"]}</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ''' | |
| # Disclaimer | |
| html += ''' | |
| <div style="margin-top:16px;padding:12px;background:#fefce8;border:1px solid #fde68a;border-radius:8px;"> | |
| <p style="font-size:11px;color:#92400e;margin:0;line-height:1.5;"> | |
| <strong>β οΈ Disclaimer:</strong> These are AI-generated suggestions based on legal templates and consumer protection standards. | |
| They are NOT legal advice. The suggested alternatives are starting points that should be reviewed and customized by a | |
| qualified attorney before use in any contract. Legal requirements vary by jurisdiction. | |
| </p> | |
| </div> | |
| ''' | |
| html += '</div>' | |
| return html | |