File size: 35,014 Bytes
8522824
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30580c9
8522824
 
30580c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
475b227
 
 
 
 
 
 
 
30580c9
 
475b227
 
 
 
 
 
 
 
 
30580c9
 
475b227
30580c9
 
 
8522824
 
 
30580c9
 
 
 
 
8522824
 
 
 
 
 
 
 
30580c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8522824
30580c9
8522824
 
 
30580c9
 
 
 
8522824
30580c9
 
8522824
 
 
 
 
 
 
 
 
 
30580c9
 
 
 
 
 
8522824
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
"""
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("<", "&lt;").replace(">", "&gt;")
        safe_text = redline["safe_alternative"].replace("<", "&lt;").replace(">", "&gt;")

        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