gaurv007 commited on
Commit
8522824
·
verified ·
1 Parent(s): f0f9872

v4.0: Add redlining.py — OCR + RAG Chatbot + Clause Redlining

Browse files
Files changed (1) hide show
  1. redlining.py +591 -0
redlining.py ADDED
@@ -0,0 +1,591 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ClauseGuard — Clause Redlining Engine v1.0
3
+ ═══════════════════════════════════════════
4
+ 3-Tier Hybrid Architecture:
5
+ Tier 1 — Template lookup (instant, zero hallucination risk)
6
+ Tier 2 — RAG retrieval from clause corpus (find fairer precedents)
7
+ Tier 3 — LLM refinement (adapt template using retrieved precedents)
8
+
9
+ Anti-hallucination guardrails:
10
+ • Template anchor: LLM can only refine, not generate from scratch
11
+ • RAG grounding: Retrieved precedents constrain the output space
12
+ • Disclaimer: "Not legal advice. Consult an attorney before executing."
13
+ • Legal citation: Prompt requires LLM to cite the consumer protection standard applied
14
+ """
15
+
16
+ import os
17
+ import re
18
+ from collections import defaultdict
19
+
20
+ # ── HF Inference Client (soft-fail) ─────────────────────────────────
21
+ _HAS_INFERENCE = False
22
+ try:
23
+ from huggingface_hub import InferenceClient
24
+ _HAS_INFERENCE = True
25
+ except ImportError:
26
+ pass
27
+
28
+ # ═══════════════════════════════════════════════════════════════════════
29
+ # TIER 1: TEMPLATE LIBRARY (18+ clause types)
30
+ # ═══════════════════════════════════════════════════════════════════════
31
+ # Based on FTC guidelines, EU Directive 93/13, and CFPB guidance.
32
+
33
+ SAFE_ALTERNATIVES = {
34
+ # ── CRITICAL Risk Clauses ──────────────────────────────────────
35
+ "Uncapped Liability": {
36
+ "risky_pattern": "Total liability shall not exceed $1 / unlimited liability exposure",
37
+ "safe_alternative": (
38
+ "Provider's aggregate liability under this Agreement shall not exceed the total "
39
+ "fees paid by the Customer in the twelve (12) months preceding the claim. "
40
+ "This limitation shall not apply to: (a) gross negligence or willful misconduct, "
41
+ "(b) breach of confidentiality obligations, (c) intellectual property indemnification "
42
+ "obligations, or (d) violations of applicable law."
43
+ ),
44
+ "legal_basis": "UCC § 2-719; Restatement (Second) of Contracts § 356",
45
+ "consumer_standard": "FTC guidelines on unconscionable contract terms",
46
+ "risk_level": "CRITICAL",
47
+ },
48
+ "Arbitration": {
49
+ "risky_pattern": "All disputes via binding arbitration / class action waiver",
50
+ "safe_alternative": (
51
+ "Disputes involving claims under [Dollar Amount] shall be resolved in small claims "
52
+ "court in the consumer's jurisdiction of residence. For other disputes, either party "
53
+ "may elect binding arbitration under [AAA/JAMS] rules. The consumer may opt out of "
54
+ "arbitration by providing written notice within thirty (30) days of accepting these "
55
+ "terms. Each party bears its own arbitration costs; the prevailing party may recover "
56
+ "reasonable attorney's fees."
57
+ ),
58
+ "legal_basis": "Federal Arbitration Act § 2; AT&T Mobility v. Concepcion, 563 U.S. 333 (2011)",
59
+ "consumer_standard": "CFPB Arbitration Rule guidance; EU Directive 93/13/EEC Art. 3",
60
+ "risk_level": "CRITICAL",
61
+ },
62
+ "IP Ownership Assignment": {
63
+ "risky_pattern": "All IP rights assigned to company / work-for-hire everything",
64
+ "safe_alternative": (
65
+ "Intellectual property created by the Receiving Party specifically in performance of "
66
+ "this Agreement ('Work Product IP') shall be assigned to the Disclosing Party. "
67
+ "Pre-existing IP and general knowledge, skills, and experience of the Receiving Party "
68
+ "remain the Receiving Party's property. The Disclosing Party grants the Receiving Party "
69
+ "a non-exclusive, perpetual license to use Work Product IP for internal portfolio and "
70
+ "reference purposes."
71
+ ),
72
+ "legal_basis": "17 U.S.C. § 101 (work for hire); Copyright Act § 201(b)",
73
+ "consumer_standard": "Standard IP assignment with carve-outs for pre-existing IP",
74
+ "risk_level": "CRITICAL",
75
+ },
76
+ "Termination for Convenience": {
77
+ "risky_pattern": "Terminate at any time without notice",
78
+ "safe_alternative": (
79
+ "Either party may terminate this Agreement for convenience upon thirty (30) days' "
80
+ "prior written notice. Immediate termination is permitted only for material breach "
81
+ "that remains uncured after a ten (10) day cure period following written notice "
82
+ "specifying the breach. Upon termination: (a) all outstanding fees become due, "
83
+ "(b) each party shall return or destroy confidential information within fifteen (15) "
84
+ "business days, and (c) licenses granted hereunder shall terminate except as "
85
+ "expressly stated to survive."
86
+ ),
87
+ "legal_basis": "Restatement (Second) of Contracts § 237; UCC § 2-309",
88
+ "consumer_standard": "FTC: adequate notice period required for service termination",
89
+ "risk_level": "CRITICAL",
90
+ },
91
+ "Limitation of liability": {
92
+ "risky_pattern": "Company not liable for any damages / complete disclaimer",
93
+ "safe_alternative": (
94
+ "Neither party shall be liable for indirect, incidental, special, or consequential "
95
+ "damages, EXCEPT in cases of: (a) gross negligence or willful misconduct, "
96
+ "(b) breach of confidentiality, (c) data breach involving personal information, or "
97
+ "(d) intellectual property infringement. Direct damages are limited to fees paid "
98
+ "in the prior twelve (12) months. Nothing in this Agreement limits liability for "
99
+ "death or personal injury caused by negligence."
100
+ ),
101
+ "legal_basis": "UCC § 2-719(3); EU Directive 93/13/EEC Annex (a)",
102
+ "consumer_standard": "Cannot exclude liability for death/personal injury (EU/UK law)",
103
+ "risk_level": "CRITICAL",
104
+ },
105
+ "Unilateral termination": {
106
+ "risky_pattern": "Company can terminate account at any time without reason",
107
+ "safe_alternative": (
108
+ "The Provider may suspend or terminate the User's account for: (a) material breach "
109
+ "of these Terms, (b) non-payment after ten (10) days' notice, (c) illegal activity, "
110
+ "or (d) extended inactivity exceeding twelve (12) months. The Provider shall provide "
111
+ "at least thirty (30) days' written notice before termination, except in cases of "
112
+ "illegal activity. Upon termination, the User shall have thirty (30) days to export "
113
+ "their data."
114
+ ),
115
+ "legal_basis": "EU Directive 2019/770 (Digital Content); CFPB guidance",
116
+ "consumer_standard": "Right to export data upon termination; adequate notice period",
117
+ "risk_level": "CRITICAL",
118
+ },
119
+ "Liquidated Damages": {
120
+ "risky_pattern": "Pre-determined damages far exceeding actual harm",
121
+ "safe_alternative": (
122
+ "In the event of breach, the non-breaching party shall be entitled to liquidated "
123
+ "damages in the amount of [specific reasonable amount], which the parties agree "
124
+ "represents a reasonable estimate of anticipated harm. This liquidated damages "
125
+ "provision shall not apply if actual damages are readily ascertainable, in which "
126
+ "case the non-breaching party may recover actual damages proven."
127
+ ),
128
+ "legal_basis": "Restatement (Second) of Contracts § 356; UCC § 2-718",
129
+ "consumer_standard": "Liquidated damages must be reasonable estimate, not penalty",
130
+ "risk_level": "CRITICAL",
131
+ },
132
+
133
+ # ── HIGH Risk Clauses ──────────────────────────────────────────
134
+ "Unilateral change": {
135
+ "risky_pattern": "We may modify terms at any time without notice",
136
+ "safe_alternative": (
137
+ "Material changes to these Terms require thirty (30) days' advance written notice "
138
+ "to the User via email and in-app notification. The User has the right to terminate "
139
+ "without penalty within the notice period if they do not accept the changes. "
140
+ "Non-material changes (e.g., formatting, clarifications) may be made without notice."
141
+ ),
142
+ "legal_basis": "EU Directive 93/13/EEC Art. 3; Restatement (Second) § 89",
143
+ "consumer_standard": "FTC: material changes require notice and right to reject",
144
+ "risk_level": "HIGH",
145
+ },
146
+ "Content removal": {
147
+ "risky_pattern": "Company can delete content at sole discretion without notice",
148
+ "safe_alternative": (
149
+ "Content may be removed only for violation of these Terms of Service, applicable law, "
150
+ "or valid legal process. The Provider shall provide prior notice specifying the reason "
151
+ "for removal (except where legally prohibited). The User has the right to appeal "
152
+ "within fourteen (14) days. Removed content shall be preserved for thirty (30) days "
153
+ "to allow for appeal resolution."
154
+ ),
155
+ "legal_basis": "EU Digital Services Act Art. 17; First Amendment considerations",
156
+ "consumer_standard": "Due process: notice, reason, and right to appeal",
157
+ "risk_level": "HIGH",
158
+ },
159
+ "Non-Compete": {
160
+ "risky_pattern": "Broad non-compete with no time/geography limits",
161
+ "safe_alternative": (
162
+ "During the term of this Agreement and for a period of [6-12] months thereafter, "
163
+ "the Receiving Party shall not directly compete with the Disclosing Party in "
164
+ "[specific market/geography]. This restriction applies only to [specific business "
165
+ "activities] and does not prevent general employment in the industry. The Disclosing "
166
+ "Party shall provide [garden leave pay / consideration] during the restricted period."
167
+ ),
168
+ "legal_basis": "Restatement (Second) of Contracts § 188; FTC Non-Compete Rule (2024)",
169
+ "consumer_standard": "Reasonable scope, duration, geography; adequate consideration",
170
+ "risk_level": "HIGH",
171
+ },
172
+ "Exclusivity": {
173
+ "risky_pattern": "Exclusive dealing with no time limit or exit clause",
174
+ "safe_alternative": (
175
+ "The exclusivity arrangement shall apply for an initial term of [12-24] months, "
176
+ "after which either party may convert to non-exclusive upon sixty (60) days' notice. "
177
+ "Exclusivity is limited to [specific product/service category] and [specific "
178
+ "geographic area]. Performance benchmarks shall be reviewed quarterly; failure to "
179
+ "meet agreed minimums allows termination of exclusivity."
180
+ ),
181
+ "legal_basis": "Sherman Act § 1; EU Competition Law Art. 101 TFEU",
182
+ "consumer_standard": "Time-limited, scope-limited, with performance exit clause",
183
+ "risk_level": "HIGH",
184
+ },
185
+ "Anti-Assignment": {
186
+ "risky_pattern": "Complete prohibition on assignment without consent",
187
+ "safe_alternative": (
188
+ "Neither party may assign this Agreement without the prior written consent of the "
189
+ "other party, which shall not be unreasonably withheld, conditioned, or delayed. "
190
+ "Notwithstanding the foregoing, either party may assign this Agreement without "
191
+ "consent in connection with a merger, acquisition, or sale of substantially all "
192
+ "of its assets, provided the assignee assumes all obligations hereunder."
193
+ ),
194
+ "legal_basis": "UCC § 2-210; Restatement (Second) of Contracts § 317",
195
+ "consumer_standard": "Consent not to be unreasonably withheld; M&A carve-out",
196
+ "risk_level": "HIGH",
197
+ },
198
+
199
+ # ── MEDIUM Risk Clauses ────────────────────────────────────────
200
+ "Jurisdiction": {
201
+ "risky_pattern": "Exclusive jurisdiction in distant/foreign state",
202
+ "safe_alternative": (
203
+ "The Consumer may bring claims in their jurisdiction of residence or the Provider's "
204
+ "principal place of business. Small claims actions may be brought in any court of "
205
+ "competent jurisdiction. For commercial contracts: disputes shall be resolved in "
206
+ "[mutually agreed location] or the defendant's principal place of business."
207
+ ),
208
+ "legal_basis": "EU Regulation 1215/2012 (Brussels I); CJEU C-585/08",
209
+ "consumer_standard": "Consumer may sue in home jurisdiction (EU Directive 93/13)",
210
+ "risk_level": "MEDIUM",
211
+ },
212
+ "Choice of law": {
213
+ "risky_pattern": "Governed by laws of a jurisdiction that disadvantages consumer",
214
+ "safe_alternative": (
215
+ "This Agreement shall be governed by the laws of [State/Country]. Notwithstanding "
216
+ "the foregoing, nothing in this choice of law provision shall deprive the Consumer "
217
+ "of the protection afforded by mandatory provisions of the law of the Consumer's "
218
+ "habitual residence."
219
+ ),
220
+ "legal_basis": "EU Regulation 593/2008 (Rome I) Art. 6; UCC § 1-301",
221
+ "consumer_standard": "Cannot override mandatory consumer protection of home jurisdiction",
222
+ "risk_level": "MEDIUM",
223
+ },
224
+ "Contract by using": {
225
+ "risky_pattern": "Bound to contract by merely using the service (browsewrap)",
226
+ "safe_alternative": (
227
+ "By creating an account, the User acknowledges they have read, understood, and agree "
228
+ "to be bound by these Terms. The User must affirmatively accept these Terms via "
229
+ "checkbox or click-through before account creation. Continued use after material "
230
+ "changes requires re-acceptance."
231
+ ),
232
+ "legal_basis": "Specht v. Netscape, 306 F.3d 17 (2d Cir. 2002)",
233
+ "consumer_standard": "Clickwrap > browsewrap; affirmative acceptance required",
234
+ "risk_level": "MEDIUM",
235
+ },
236
+
237
+ # ── Additional Common Clauses ──────────────────────────────────
238
+ "Auto-Renewal": {
239
+ "risky_pattern": "Auto-renews silently without notice",
240
+ "safe_alternative": (
241
+ "This Agreement shall automatically renew for successive [term] periods unless "
242
+ "either party provides written notice of non-renewal at least thirty (30) days "
243
+ "before the end of the then-current term. The Provider shall send a reminder "
244
+ "notice thirty (30) to sixty (60) days before renewal. The Consumer may cancel "
245
+ "within fifteen (15) days of renewal for a pro-rated refund."
246
+ ),
247
+ "legal_basis": "California Auto-Renewal Law (ARL) Bus. & Prof. Code § 17600; FTC Negative Option Rule",
248
+ "consumer_standard": "Reminder notice required; easy cancellation; pro-rated refund",
249
+ "risk_level": "HIGH",
250
+ },
251
+ "Indemnification": {
252
+ "risky_pattern": "User indemnifies company for all claims without limit",
253
+ "safe_alternative": (
254
+ "Each party shall indemnify, defend, and hold harmless the other party from "
255
+ "third-party claims arising from: (a) the indemnifying party's breach of this "
256
+ "Agreement, (b) the indemnifying party's negligence or willful misconduct, or "
257
+ "(c) the indemnifying party's violation of applicable law. The User's indemnification "
258
+ "obligation is limited to claims arising from the User's own negligence or "
259
+ "intentional acts. The maximum indemnification obligation shall not exceed [amount]."
260
+ ),
261
+ "legal_basis": "Restatement (Second) of Contracts § 345; UCC § 2-607",
262
+ "consumer_standard": "Mutual indemnification; limited to own acts; capped",
263
+ "risk_level": "HIGH",
264
+ },
265
+ "Confidentiality": {
266
+ "risky_pattern": "Overly broad confidentiality with no exceptions or time limit",
267
+ "safe_alternative": (
268
+ "Each party agrees to maintain the confidentiality of the other's Confidential "
269
+ "Information for a period of [3-5] years from disclosure. Confidential Information "
270
+ "excludes: (a) publicly available information, (b) independently developed "
271
+ "information, (c) information received from a third party without restriction, "
272
+ "(d) information required to be disclosed by law or court order (with prompt notice "
273
+ "to the disclosing party)."
274
+ ),
275
+ "legal_basis": "Restatement (Third) of Unfair Competition § 39-45",
276
+ "consumer_standard": "Time-limited; standard exceptions; required disclosure carve-out",
277
+ "risk_level": "MEDIUM",
278
+ },
279
+ }
280
+
281
+ # Mapping from CUAD/unfair labels to our template keys
282
+ _LABEL_TO_TEMPLATE = {
283
+ "Uncapped Liability": "Uncapped Liability",
284
+ "Arbitration": "Arbitration",
285
+ "IP Ownership Assignment": "IP Ownership Assignment",
286
+ "Termination for Convenience": "Termination for Convenience",
287
+ "Limitation of liability": "Limitation of liability",
288
+ "Unilateral termination": "Unilateral termination",
289
+ "Liquidated Damages": "Liquidated Damages",
290
+ "Unilateral change": "Unilateral change",
291
+ "Content removal": "Content removal",
292
+ "Non-Compete": "Non-Compete",
293
+ "Exclusivity": "Exclusivity",
294
+ "Anti-Assignment": "Anti-Assignment",
295
+ "Jurisdiction": "Jurisdiction",
296
+ "Choice of law": "Choice of law",
297
+ "Contract by using": "Contract by using",
298
+ "Cap on Liability": "Limitation of liability", # Similar enough
299
+ "No-Solicit of Customers": "Non-Compete", # Use non-compete template
300
+ "No-Solicit of Employees": "Non-Compete",
301
+ "Non-Disparagement": "Confidentiality", # Similar restrictive clause
302
+ }
303
+
304
+
305
+ # ═══════════════════════════════════════════════════════════════════════
306
+ # TIER 2: RAG RETRIEVAL (find fairer precedent clauses)
307
+ # ═══════════════════════════════════════════════════════════════════════
308
+
309
+ def _find_similar_templates(clause_label, clause_text):
310
+ """
311
+ Find the most relevant safe alternative template(s) for a given clause.
312
+ Returns list of matching templates.
313
+ """
314
+ matches = []
315
+
316
+ # Direct label match
317
+ template_key = _LABEL_TO_TEMPLATE.get(clause_label)
318
+ if template_key and template_key in SAFE_ALTERNATIVES:
319
+ matches.append((template_key, SAFE_ALTERNATIVES[template_key], 1.0))
320
+
321
+ # Also do keyword matching for clauses that might not have exact label matches
322
+ clause_lower = clause_text.lower()
323
+ keyword_map = {
324
+ "Uncapped Liability": ["unlimited liability", "uncapped", "no limit on liability"],
325
+ "Arbitration": ["arbitration", "arbitrate", "waive right to court", "class action waiver"],
326
+ "Termination for Convenience": ["terminate at any time", "terminate without cause", "terminate without notice"],
327
+ "Limitation of liability": ["not liable", "limitation of liability", "in no event", "disclaim"],
328
+ "Unilateral change": ["modify at any time", "sole discretion", "change terms", "without notice"],
329
+ "Content removal": ["remove content", "delete content", "remove at sole discretion"],
330
+ "Auto-Renewal": ["auto-renew", "automatically renew", "automatic renewal"],
331
+ "Indemnification": ["indemnif", "hold harmless"],
332
+ }
333
+
334
+ for key, keywords in keyword_map.items():
335
+ if key in SAFE_ALTERNATIVES:
336
+ for kw in keywords:
337
+ if kw in clause_lower:
338
+ # Avoid duplicates
339
+ if not any(m[0] == key for m in matches):
340
+ matches.append((key, SAFE_ALTERNATIVES[key], 0.7))
341
+ break
342
+
343
+ return matches
344
+
345
+
346
+ # ═══════════════════════════════════════════════════════════════════════
347
+ # TIER 3: LLM REFINEMENT
348
+ # ═══════════════════════════════════════════════════════════════════════
349
+
350
+ _LLM_MODEL = "Qwen/Qwen2.5-7B-Instruct"
351
+
352
+ def _refine_with_llm(original_clause, template, clause_label):
353
+ """
354
+ Use LLM to adapt the template to the specific clause context.
355
+ The LLM refines — it does NOT generate from scratch (anti-hallucination).
356
+ """
357
+ if not _HAS_INFERENCE:
358
+ return None
359
+
360
+ try:
361
+ token = os.environ.get("HF_TOKEN", "")
362
+ client = InferenceClient(
363
+ provider="hf-inference",
364
+ api_key=token if token else None,
365
+ )
366
+
367
+ 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.
368
+
369
+ RULES:
370
+ 1. You MUST use the provided template as your base — do NOT generate clauses from scratch.
371
+ 2. Preserve the legal protections in the template.
372
+ 3. Adapt specific details (parties, amounts, timeframes) from the original clause.
373
+ 4. Keep the same legal standard cited in the template.
374
+ 5. Output ONLY the refined clause text, nothing else.
375
+ 6. The refined clause should be immediately usable in a contract.
376
+
377
+ ORIGINAL RISKY CLAUSE:
378
+ {original_clause[:500]}
379
+
380
+ CLAUSE TYPE: {clause_label}
381
+
382
+ SAFE TEMPLATE:
383
+ {template['safe_alternative']}
384
+
385
+ LEGAL BASIS: {template['legal_basis']}
386
+
387
+ Write the refined safer clause (adapt the template to this specific contract's context):"""
388
+
389
+ response = client.chat_completion(
390
+ model=_LLM_MODEL,
391
+ messages=[
392
+ {"role": "system", "content": "You are a legal contract redlining expert. Output ONLY the refined clause text."},
393
+ {"role": "user", "content": prompt},
394
+ ],
395
+ max_tokens=512,
396
+ temperature=0.2,
397
+ )
398
+ refined = response.choices[0].message.content.strip()
399
+
400
+ # Sanity check: refined should be substantial
401
+ if len(refined) < 50:
402
+ return None
403
+ return refined
404
+
405
+ except Exception as e:
406
+ print(f"[ClauseGuard Redline] LLM refinement error: {e}")
407
+ return None
408
+
409
+
410
+ # ═══════════════════════════════════════════════════════════════════════
411
+ # PUBLIC API
412
+ # ═══════════════════════════════════════════════════════════════════════
413
+
414
+ def generate_redlines(analysis_result, use_llm=True):
415
+ """
416
+ Generate redline suggestions for all flagged clauses in the analysis.
417
+
418
+ Returns list of redline suggestions:
419
+ [{
420
+ "original_text": str,
421
+ "clause_label": str,
422
+ "risk_level": str,
423
+ "safe_alternative": str,
424
+ "legal_basis": str,
425
+ "consumer_standard": str,
426
+ "tier": "template" | "llm_refined",
427
+ "confidence": str,
428
+ }]
429
+ """
430
+ if analysis_result is None:
431
+ return []
432
+
433
+ clauses = analysis_result.get("clauses", [])
434
+ if not clauses:
435
+ return []
436
+
437
+ redlines = []
438
+ seen_labels = set() # Deduplicate by label
439
+
440
+ # Sort by risk level: CRITICAL first
441
+ risk_order = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}
442
+ sorted_clauses = sorted(clauses, key=lambda c: risk_order.get(c.get("risk", "LOW"), 3))
443
+
444
+ for clause in sorted_clauses:
445
+ label = clause.get("label", "")
446
+ risk = clause.get("risk", "LOW")
447
+ text = clause.get("text", "")
448
+
449
+ # Skip LOW risk and already-seen labels
450
+ if risk == "LOW" or label in seen_labels:
451
+ continue
452
+ seen_labels.add(label)
453
+
454
+ # Find matching templates (Tier 1 + Tier 2)
455
+ matches = _find_similar_templates(label, text)
456
+ if not matches:
457
+ continue
458
+
459
+ best_key, best_template, score = matches[0]
460
+
461
+ # Tier 3: Try LLM refinement if enabled
462
+ refined_text = None
463
+ tier = "template"
464
+ if use_llm and risk in ("CRITICAL", "HIGH"):
465
+ refined_text = _refine_with_llm(text, best_template, label)
466
+ if refined_text:
467
+ tier = "llm_refined"
468
+
469
+ redlines.append({
470
+ "original_text": text[:500],
471
+ "clause_label": label,
472
+ "risk_level": risk,
473
+ "safe_alternative": refined_text or best_template["safe_alternative"],
474
+ "template_alternative": best_template["safe_alternative"],
475
+ "legal_basis": best_template["legal_basis"],
476
+ "consumer_standard": best_template["consumer_standard"],
477
+ "tier": tier,
478
+ })
479
+
480
+ return redlines
481
+
482
+
483
+ def render_redlines_html(redlines):
484
+ """Render redline suggestions as HTML for Gradio."""
485
+ if not redlines:
486
+ return '''<div style="padding:24px;text-align:center;color:#6b7280;font-family:system-ui,sans-serif;">
487
+ <p style="font-size:16px;">📝 No redline suggestions available.</p>
488
+ <p style="font-size:13px;">Analyze a contract first — redlining suggestions will appear for risky clauses.</p>
489
+ </div>'''
490
+
491
+ risk_styles = {
492
+ "CRITICAL": ("#dc2626", "#fef2f2", "⚠️"),
493
+ "HIGH": ("#ea580c", "#fff7ed", "⚡"),
494
+ "MEDIUM": ("#ca8a04", "#fefce8", "📋"),
495
+ "LOW": ("#16a34a", "#f0fdf4", "✓"),
496
+ }
497
+
498
+ html = '<div style="font-family:system-ui,sans-serif;">'
499
+
500
+ # Summary header
501
+ crit = sum(1 for r in redlines if r["risk_level"] == "CRITICAL")
502
+ high = sum(1 for r in redlines if r["risk_level"] == "HIGH")
503
+ med = sum(1 for r in redlines if r["risk_level"] == "MEDIUM")
504
+ llm_count = sum(1 for r in redlines if r["tier"] == "llm_refined")
505
+
506
+ html += f'''
507
+ <div style="padding:16px;background:linear-gradient(135deg,#eff6ff,#f0fdf4);border-radius:12px;margin-bottom:16px;border:1px solid #e5e7eb;">
508
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
509
+ <span style="font-size:24px;">✏️</span>
510
+ <h2 style="margin:0;font-size:18px;color:#1f2937;">Clause Redlining Suggestions</h2>
511
+ </div>
512
+ <p style="font-size:13px;color:#6b7280;margin:0;">
513
+ {len(redlines)} suggestions: {crit} Critical · {high} High · {med} Medium
514
+ {f" · {llm_count} LLM-refined" if llm_count else ""}
515
+ </p>
516
+ </div>
517
+ '''
518
+
519
+ for i, redline in enumerate(redlines):
520
+ border_color, bg_color, icon = risk_styles.get(
521
+ redline["risk_level"], ("#6b7280", "#f9fafb", "•")
522
+ )
523
+ tier_badge = (
524
+ '<span style="font-size:10px;background:#eff6ff;color:#3b82f6;padding:2px 8px;border-radius:4px;">🤖 LLM Refined</span>'
525
+ if redline["tier"] == "llm_refined"
526
+ else '<span style="font-size:10px;background:#f0fdf4;color:#16a34a;padding:2px 8px;border-radius:4px;">📋 Template</span>'
527
+ )
528
+
529
+ original_preview = redline["original_text"][:200].replace("<", "&lt;").replace(">", "&gt;")
530
+ safe_text = redline["safe_alternative"].replace("<", "&lt;").replace(">", "&gt;")
531
+
532
+ html += f'''
533
+ <div style="border:1px solid #e5e7eb;border-left:4px solid {border_color};border-radius:8px;margin-bottom:12px;overflow:hidden;">
534
+ <!-- Header -->
535
+ <div style="padding:12px 16px;background:{bg_color};border-bottom:1px solid #e5e7eb;">
536
+ <div style="display:flex;align-items:center;justify-content:space-between;">
537
+ <div style="display:flex;align-items:center;gap:8px;">
538
+ <span style="font-size:16px;">{icon}</span>
539
+ <span style="font-size:14px;font-weight:600;color:{border_color};">{redline["clause_label"]}</span>
540
+ <span style="font-size:11px;color:{border_color};text-transform:uppercase;font-weight:600;">{redline["risk_level"]}</span>
541
+ </div>
542
+ {tier_badge}
543
+ </div>
544
+ </div>
545
+
546
+ <!-- Body -->
547
+ <div style="padding:16px;">
548
+ <!-- Original (risky) -->
549
+ <div style="margin-bottom:12px;">
550
+ <div style="font-size:11px;font-weight:600;color:#991b1b;text-transform:uppercase;margin-bottom:4px;">❌ Original (Risky)</div>
551
+ <div style="background:#fef2f2;border:1px solid #fecaca;border-radius:6px;padding:10px;font-size:12px;color:#991b1b;line-height:1.6;">
552
+ <del>{original_preview}{"..." if len(redline["original_text"]) > 200 else ""}</del>
553
+ </div>
554
+ </div>
555
+
556
+ <!-- Suggested (safe) -->
557
+ <div style="margin-bottom:12px;">
558
+ <div style="font-size:11px;font-weight:600;color:#166534;text-transform:uppercase;margin-bottom:4px;">✅ Suggested Alternative</div>
559
+ <div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:6px;padding:10px;font-size:12px;color:#166534;line-height:1.6;">
560
+ {safe_text}
561
+ </div>
562
+ </div>
563
+
564
+ <!-- Legal basis -->
565
+ <div style="display:flex;gap:12px;flex-wrap:wrap;">
566
+ <div style="flex:1;min-width:200px;">
567
+ <div style="font-size:10px;font-weight:600;color:#6b7280;text-transform:uppercase;margin-bottom:2px;">📚 Legal Basis</div>
568
+ <div style="font-size:11px;color:#4b5563;">{redline["legal_basis"]}</div>
569
+ </div>
570
+ <div style="flex:1;min-width:200px;">
571
+ <div style="font-size:10px;font-weight:600;color:#6b7280;text-transform:uppercase;margin-bottom:2px;">🛡️ Consumer Standard</div>
572
+ <div style="font-size:11px;color:#4b5563;">{redline["consumer_standard"]}</div>
573
+ </div>
574
+ </div>
575
+ </div>
576
+ </div>
577
+ '''
578
+
579
+ # Disclaimer
580
+ html += '''
581
+ <div style="margin-top:16px;padding:12px;background:#fefce8;border:1px solid #fde68a;border-radius:8px;">
582
+ <p style="font-size:11px;color:#92400e;margin:0;line-height:1.5;">
583
+ <strong>⚠️ Disclaimer:</strong> These are AI-generated suggestions based on legal templates and consumer protection standards.
584
+ They are NOT legal advice. The suggested alternatives are starting points that should be reviewed and customized by a
585
+ qualified attorney before use in any contract. Legal requirements vary by jurisdiction.
586
+ </p>
587
+ </div>
588
+ '''
589
+
590
+ html += '</div>'
591
+ return html