ClauseGuard / redlining.py
gaurv007's picture
v4.0: Add redlining.py β€” OCR + RAG Chatbot + Clause Redlining
8522824 verified
raw
history blame
31.4 kB
"""
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
# ═══════════════════════════════════════════════════════════════════════
# PUBLIC API
# ═══════════════════════════════════════════════════════════════════════
def generate_redlines(analysis_result, use_llm=True):
"""
Generate redline suggestions for all flagged clauses in the analysis.
Returns list of redline suggestions:
[{
"original_text": str,
"clause_label": str,
"risk_level": str,
"safe_alternative": str,
"legal_basis": str,
"consumer_standard": str,
"tier": "template" | "llm_refined",
"confidence": str,
}]
"""
if analysis_result is None:
return []
clauses = analysis_result.get("clauses", [])
if not clauses:
return []
redlines = []
seen_labels = set() # Deduplicate by label
# Sort by risk level: CRITICAL first
risk_order = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}
sorted_clauses = sorted(clauses, key=lambda c: risk_order.get(c.get("risk", "LOW"), 3))
for clause in sorted_clauses:
label = clause.get("label", "")
risk = clause.get("risk", "LOW")
text = clause.get("text", "")
# Skip LOW risk and already-seen labels
if risk == "LOW" or label in seen_labels:
continue
seen_labels.add(label)
# Find matching templates (Tier 1 + Tier 2)
matches = _find_similar_templates(label, text)
if not matches:
continue
best_key, best_template, score = matches[0]
# 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