narcolepticchicken commited on
Commit
2f02fd3
·
verified ·
1 Parent(s): d8760dd

Upload drafting_engine.py

Browse files
Files changed (1) hide show
  1. drafting_engine.py +296 -0
drafting_engine.py ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Contract Drafting Engine.
3
+ Orchestrates clause retrieval, playbook rules, fallback positions,
4
+ risk flags, drafting checklist, and verifier pass.
5
+ """
6
+
7
+ import json
8
+ from typing import List, Dict, Optional, Any
9
+ from dataclasses import dataclass, asdict
10
+
11
+ from playbook import (
12
+ get_required_clauses,
13
+ get_fallback_position,
14
+ get_risk_flags,
15
+ get_checklist,
16
+ )
17
+ from clause_retriever import ClauseRetriever
18
+
19
+
20
+ @dataclass
21
+ class DraftingContext:
22
+ contract_type: str
23
+ party_position: str # pro_company, pro_counterparty, balanced
24
+ deal_context: str
25
+ business_constraints: List[str]
26
+ governing_law: Optional[str] = None
27
+ counterparty_name: Optional[str] = None
28
+ company_name: Optional[str] = None
29
+ deal_value: Optional[str] = None
30
+ term_length: Optional[str] = None
31
+
32
+
33
+ @dataclass
34
+ class DraftedClause:
35
+ clause_name: str
36
+ clause_text: str
37
+ source: str
38
+ fallback_applied: bool
39
+ risk_flags: List[Dict[str, str]]
40
+ checklist_items: List[str]
41
+ retrieved_clauses: List[Dict]
42
+
43
+
44
+ @dataclass
45
+ class DraftedContract:
46
+ contract_type: str
47
+ context: DraftingContext
48
+ clauses: List[DraftedClause]
49
+ risk_flags: List[Dict[str, Any]]
50
+ checklist: List[Dict[str, Any]]
51
+ verifier_notes: List[str]
52
+
53
+
54
+ class ContractDraftingEngine:
55
+ def __init__(
56
+ self,
57
+ retriever: Optional[ClauseRetriever] = None,
58
+ generator_model_name: Optional[str] = None,
59
+ ):
60
+ self.retriever = retriever or ClauseRetriever()
61
+ self.generator_model_name = generator_model_name
62
+ self.generator = None
63
+ if generator_model_name:
64
+ try:
65
+ from transformers import pipeline
66
+ self.generator = pipeline(
67
+ "text-generation",
68
+ model=generator_model_name,
69
+ device_map="auto",
70
+ )
71
+ except Exception as e:
72
+ print(f"Warning: could not load generator {generator_model_name}: {e}")
73
+
74
+ def draft(self, context: DraftingContext) -> DraftedContract:
75
+ required = get_required_clauses(context.contract_type)
76
+ checklist = get_checklist(context.contract_type)
77
+ drafted_clauses: List[DraftedClause] = []
78
+ all_risk_flags: List[Dict[str, Any]] = []
79
+
80
+ for clause_name in required:
81
+ # Retrieve precedent clauses
82
+ query = f"{clause_name.replace('_', ' ')} clause for {context.contract_type.replace('_', ' ')}"
83
+ retrieved = self.retriever.retrieve(
84
+ query=query,
85
+ clause_type=clause_name,
86
+ top_k=3,
87
+ )
88
+
89
+ # Get fallback position
90
+ fallback = get_fallback_position(clause_name, context.party_position)
91
+
92
+ # Generate clause text
93
+ clause_text = self._generate_clause(
94
+ clause_name=clause_name,
95
+ context=context,
96
+ retrieved_clauses=retrieved,
97
+ fallback=fallback,
98
+ )
99
+
100
+ # Get risk flags
101
+ flags = get_risk_flags(clause_name)
102
+ # Apply contextual risk analysis
103
+ active_flags = self._evaluate_risk_flags(clause_text, flags, context)
104
+ all_risk_flags.extend([
105
+ {"clause": clause_name, **f} for f in active_flags
106
+ ])
107
+
108
+ # Checklist items for this clause
109
+ clause_checklist = [
110
+ c["item"] for c in checklist
111
+ if clause_name.replace("_", " ") in c["item"].lower()
112
+ or c["category"] in clause_name
113
+ ]
114
+
115
+ drafted_clauses.append(DraftedClause(
116
+ clause_name=clause_name,
117
+ clause_text=clause_text,
118
+ source="retrieved+generated",
119
+ fallback_applied=fallback is not None,
120
+ risk_flags=active_flags,
121
+ checklist_items=clause_checklist,
122
+ retrieved_clauses=retrieved,
123
+ ))
124
+
125
+ # Verifier pass
126
+ verifier_notes = self._verifier_pass(drafted_clauses, context)
127
+
128
+ return DraftedContract(
129
+ contract_type=context.contract_type,
130
+ context=context,
131
+ clauses=drafted_clauses,
132
+ risk_flags=all_risk_flags,
133
+ checklist=[{"item": c["item"], "category": c["category"], "checked": False} for c in checklist],
134
+ verifier_notes=verifier_notes,
135
+ )
136
+
137
+ def _generate_clause(
138
+ self,
139
+ clause_name: str,
140
+ context: DraftingContext,
141
+ retrieved_clauses: List[Dict],
142
+ fallback: Optional[Dict[str, str]],
143
+ ) -> str:
144
+ # Build prompt from retrieved clauses + playbook
145
+ prompt_parts = [
146
+ f"Draft a {clause_name.replace('_', ' ')} clause for a {context.contract_type.replace('_', ' ')}.",
147
+ f"Party position: {context.party_position}.",
148
+ f"Deal context: {context.deal_context}",
149
+ ]
150
+ if fallback:
151
+ prompt_parts.append(f"Fallback position: {json.dumps(fallback)}")
152
+ if retrieved_clauses:
153
+ prompt_parts.append("Precedent clauses:")
154
+ for rc in retrieved_clauses:
155
+ prompt_parts.append(f"- {rc['clause_text'][:500]}")
156
+ prompt = "\n".join(prompt_parts)
157
+
158
+ if self.generator:
159
+ try:
160
+ out = self.generator(
161
+ prompt,
162
+ max_new_tokens=512,
163
+ do_sample=True,
164
+ temperature=0.3,
165
+ )
166
+ return out[0]["generated_text"][len(prompt):].strip()
167
+ except Exception as e:
168
+ print(f"Generation failed for {clause_name}: {e}")
169
+
170
+ # Fallback: template-based generation
171
+ return self._template_clause(clause_name, context, fallback)
172
+
173
+ def _template_clause(
174
+ self,
175
+ clause_name: str,
176
+ context: DraftingContext,
177
+ fallback: Optional[Dict[str, str]],
178
+ ) -> str:
179
+ """Simple template-based clause generation when no LLM is available."""
180
+ templates = {
181
+ "limitation_of_liability": (
182
+ "LIMITATION OF LIABILITY. "
183
+ "Except for breaches of confidentiality, IP infringement, or gross negligence, "
184
+ "each party's aggregate liability arising out of this agreement shall not exceed "
185
+ f"{fallback.get('cap', 'the fees paid in the 12 months preceding the claim') if fallback else 'the fees paid in the 12 months preceding the claim'}."
186
+ ),
187
+ "indemnification": (
188
+ "INDEMNIFICATION. "
189
+ f"{context.company_name or 'Company'} shall indemnify {context.counterparty_name or 'Counterparty'} against third-party claims arising from "
190
+ f"{fallback.get('scope', 'IP infringement and breach of confidentiality') if fallback else 'IP infringement and breach of confidentiality'}."
191
+ ),
192
+ "data_protection": (
193
+ "DATA PROTECTION. "
194
+ "Each party shall process personal data in accordance with applicable data protection laws. "
195
+ f"{fallback.get('role', 'The parties shall act as independent controllers') if fallback else 'The parties shall act as independent controllers'}."
196
+ ),
197
+ "termination": (
198
+ "TERMINATION. "
199
+ f"Either party may terminate this agreement {fallback.get('for_convenience', 'for convenience with 60 days notice') if fallback else 'for convenience with 60 days notice'}. "
200
+ "Upon termination, all fees owed survive and data shall be returned within 30 days."
201
+ ),
202
+ "intellectual_property": (
203
+ "INTELLECTUAL PROPERTY. "
204
+ f"{fallback.get('ownership', 'Each party retains its pre-existing IP') if fallback else 'Each party retains its pre-existing IP'}. "
205
+ "All custom deliverables shall be owned as specified in the applicable SOW."
206
+ ),
207
+ "confidentiality": (
208
+ "CONFIDENTIALITY. "
209
+ "Each party agrees to hold all Confidential Information in strict confidence and not disclose it to any third parties except as required by law."
210
+ ),
211
+ "governing_law": (
212
+ f"GOVERNING LAW. This agreement shall be governed by the laws of {context.governing_law or 'the State of Delaware'}, without regard to conflict of laws principles."
213
+ ),
214
+ }
215
+ return templates.get(
216
+ clause_name,
217
+ f"[{clause_name.replace('_', ' ').title()}]. [Placeholder clause for {context.contract_type}.]"
218
+ )
219
+
220
+ def _evaluate_risk_flags(
221
+ self,
222
+ clause_text: str,
223
+ flags: List[Dict[str, str]],
224
+ context: DraftingContext,
225
+ ) -> List[Dict[str, str]]:
226
+ active = []
227
+ text_lower = clause_text.lower()
228
+ for flag in flags:
229
+ if flag["flag"] == "NO_CAP" and "cap" not in text_lower and "limited" not in text_lower:
230
+ active.append(flag)
231
+ elif flag["flag"] == "NO_IP_CARVEOUT" and "intellectual property" not in text_lower and "ip" not in text_lower:
232
+ active.append(flag)
233
+ elif flag["flag"] == "NO_DPA" and "data processing" not in text_lower and "dpa" not in text_lower:
234
+ active.append(flag)
235
+ elif flag["flag"] == "NO_CURE_PERIOD" and "cure" not in text_lower:
236
+ active.append(flag)
237
+ elif flag["flag"] == "NO_DATA_RETURN" and "return" not in text_lower and "delete" not in text_lower:
238
+ active.append(flag)
239
+ elif flag["flag"] == "NO_MUTUALITY" and "mutual" not in text_lower:
240
+ active.append(flag)
241
+ return active
242
+
243
+ def _verifier_pass(
244
+ self,
245
+ clauses: List[DraftedClause],
246
+ context: DraftingContext,
247
+ ) -> List[str]:
248
+ notes = []
249
+ clause_names = {c.clause_name for c in clauses}
250
+ required = set(get_required_clauses(context.contract_type))
251
+ missing = required - clause_names
252
+ if missing:
253
+ notes.append(f"MISSING CLAUSES: {', '.join(missing)}")
254
+
255
+ # Check internal consistency
256
+ has_limitation = any(c.clause_name == "limitation_of_liability" for c in clauses)
257
+ has_indemnity = any(c.clause_name == "indemnification" for c in clauses)
258
+ if has_limitation and has_indemnity:
259
+ notes.append("PASS: Limitation and indemnification both present.")
260
+ if not has_limitation:
261
+ notes.append("WARNING: No limitation of liability clause.")
262
+
263
+ # Check for invented terms (basic heuristic)
264
+ for c in clauses:
265
+ if "[Placeholder" in c.clause_text:
266
+ notes.append(f"WARNING: {c.clause_name} contains placeholder text.")
267
+
268
+ return notes
269
+
270
+ def export(self, contract: DraftedContract, fmt: str = "json") -> str:
271
+ if fmt == "json":
272
+ return json.dumps(asdict(contract), indent=2)
273
+ elif fmt == "markdown":
274
+ lines = [
275
+ f"# {contract.contract_type.replace('_', ' ').title()}",
276
+ "",
277
+ "## Context",
278
+ f"- Party position: {contract.context.party_position}",
279
+ f"- Deal context: {contract.context.deal_context}",
280
+ "",
281
+ "## Clauses",
282
+ ]
283
+ for c in contract.clauses:
284
+ lines.append(f"### {c.clause_name.replace('_', ' ').title()}")
285
+ lines.append(c.clause_text)
286
+ lines.append("")
287
+ lines.append("## Risk Flags")
288
+ for rf in contract.risk_flags:
289
+ lines.append(f"- **{rf['severity']}** [{rf['clause']}]: {rf['description']}")
290
+ lines.append("")
291
+ lines.append("## Verifier Notes")
292
+ for note in contract.verifier_notes:
293
+ lines.append(f"- {note}")
294
+ return "\n".join(lines)
295
+ else:
296
+ raise ValueError(f"Unknown format: {fmt}")