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

Upload clause_retriever.py

Browse files
Files changed (1) hide show
  1. clause_retriever.py +161 -0
clause_retriever.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Clause retrieval module.
3
+ Builds a BM25 + embedding index over a clause corpus.
4
+ Retrieves relevant precedent clauses for a drafting query.
5
+ """
6
+
7
+ import json
8
+ import pickle
9
+ from typing import List, Dict, Tuple, Optional
10
+ import numpy as np
11
+
12
+ try:
13
+ from rank_bm25 import BM25Okapi
14
+ except ImportError:
15
+ BM25Okapi = None
16
+
17
+ try:
18
+ from sentence_transformers import SentenceTransformer, util
19
+ except ImportError:
20
+ SentenceTransformer = None
21
+
22
+
23
+ class ClauseRetriever:
24
+ def __init__(
25
+ self,
26
+ embedding_model_name: str = "sentence-transformers/all-MiniLM-L6-v2",
27
+ use_bm25: bool = True,
28
+ use_embeddings: bool = True,
29
+ ):
30
+ self.use_bm25 = use_bm25 and BM25Okapi is not None
31
+ self.use_embeddings = use_embeddings and SentenceTransformer is not None
32
+ self.embedding_model_name = embedding_model_name
33
+ self.bm25 = None
34
+ self.corpus: List[Dict] = []
35
+ self.tokenized_corpus: List[List[str]] = []
36
+ self.embeddings: Optional[np.ndarray] = None
37
+ self.embedding_model = None
38
+ if self.use_embeddings:
39
+ self.embedding_model = SentenceTransformer(embedding_model_name)
40
+
41
+ def _tokenize(self, text: str) -> List[str]:
42
+ return text.lower().split()
43
+
44
+ def add_clauses(self, clauses: List[Dict[str, str]]):
45
+ """
46
+ clauses: list of dicts with keys 'clause_text', 'clause_type', 'source', etc.
47
+ """
48
+ self.corpus.extend(clauses)
49
+ if self.use_bm25:
50
+ self.tokenized_corpus = [self._tokenize(c["clause_text"]) for c in self.corpus]
51
+ self.bm25 = BM25Okapi(self.tokenized_corpus)
52
+ if self.use_embeddings and self.embedding_model is not None:
53
+ texts = [c["clause_text"] for c in self.corpus]
54
+ self.embeddings = self.embedding_model.encode(
55
+ texts, show_progress_bar=True, convert_to_numpy=True
56
+ )
57
+
58
+ def retrieve(
59
+ self,
60
+ query: str,
61
+ clause_type: Optional[str] = None,
62
+ top_k: int = 5,
63
+ bm25_weight: float = 0.3,
64
+ embedding_weight: float = 0.7,
65
+ ) -> List[Dict]:
66
+ if not self.corpus:
67
+ return []
68
+ scores = np.zeros(len(self.corpus))
69
+ if self.use_bm25 and self.bm25 is not None:
70
+ tokenized_query = self._tokenize(query)
71
+ bm25_scores = np.array(self.bm25.get_scores(tokenized_query))
72
+ if bm25_scores.max() > 0:
73
+ bm25_scores = bm25_scores / bm25_scores.max()
74
+ scores += bm25_weight * bm25_scores
75
+ if self.use_embeddings and self.embedding_model is not None and self.embeddings is not None:
76
+ query_emb = self.embedding_model.encode(query, convert_to_numpy=True)
77
+ sims = util.cos_sim(query_emb, self.embeddings)[0].cpu().numpy()
78
+ scores += embedding_weight * sims
79
+ # Filter by clause_type if requested
80
+ indices = list(range(len(self.corpus)))
81
+ if clause_type:
82
+ indices = [i for i in indices if self.corpus[i].get("clause_type") == clause_type]
83
+ ranked = sorted(indices, key=lambda i: scores[i], reverse=True)[:top_k]
84
+ results = []
85
+ for i in ranked:
86
+ item = dict(self.corpus[i])
87
+ item["score"] = float(scores[i])
88
+ results.append(item)
89
+ return results
90
+
91
+ def save(self, path_prefix: str):
92
+ meta = {
93
+ "corpus": self.corpus,
94
+ "embedding_model_name": self.embedding_model_name,
95
+ "use_bm25": self.use_bm25,
96
+ "use_embeddings": self.use_embeddings,
97
+ }
98
+ with open(path_prefix + "_meta.json", "w") as f:
99
+ json.dump(meta, f)
100
+ if self.embeddings is not None:
101
+ np.save(path_prefix + "_embeddings.npy", self.embeddings)
102
+ if self.bm25 is not None:
103
+ with open(path_prefix + "_bm25.pkl", "wb") as f:
104
+ pickle.dump(self.bm25, f)
105
+
106
+ def load(self, path_prefix: str):
107
+ with open(path_prefix + "_meta.json", "r") as f:
108
+ meta = json.load(f)
109
+ self.corpus = meta["corpus"]
110
+ self.embedding_model_name = meta["embedding_model_name"]
111
+ self.use_bm25 = meta["use_bm25"]
112
+ self.use_embeddings = meta["use_embeddings"]
113
+ if self.use_bm25:
114
+ with open(path_prefix + "_bm25.pkl", "rb") as f:
115
+ self.bm25 = pickle.load(f)
116
+ self.tokenized_corpus = [self._tokenize(c["clause_text"]) for c in self.corpus]
117
+ if self.use_embeddings:
118
+ self.embeddings = np.load(path_prefix + "_embeddings.npy")
119
+ if self.embedding_model is None:
120
+ self.embedding_model = SentenceTransformer(self.embedding_model_name)
121
+
122
+
123
+ def build_retriever_from_hf_datasets(
124
+ clause_dataset_name: str = "asapworks/Contract_Clause_SampleDataset",
125
+ contract_dataset_name: str = "albertvillanova/legal_contracts",
126
+ max_contracts: int = 500,
127
+ max_clauses_per_contract: int = 20,
128
+ ) -> ClauseRetriever:
129
+ from datasets import load_dataset
130
+ retriever = ClauseRetriever()
131
+ # Load labeled clause dataset
132
+ try:
133
+ ds = load_dataset(clause_dataset_name, split="train")
134
+ for row in ds:
135
+ retriever.add_clauses([{
136
+ "clause_text": row["clause_text"],
137
+ "clause_type": row.get("clause_type", "unknown"),
138
+ "source": row.get("file", clause_dataset_name),
139
+ }])
140
+ except Exception as e:
141
+ print(f"Warning: could not load {clause_dataset_name}: {e}")
142
+ # Load raw contracts and chunk for retrieval corpus
143
+ try:
144
+ ds = load_dataset(contract_dataset_name, split="train", streaming=True)
145
+ count = 0
146
+ for row in ds:
147
+ text = row["text"]
148
+ # Simple paragraph chunking
149
+ paragraphs = [p.strip() for p in text.split("\n\n") if len(p.strip()) > 100]
150
+ for para in paragraphs[:max_clauses_per_contract]:
151
+ retriever.add_clauses([{
152
+ "clause_text": para,
153
+ "clause_type": "unknown",
154
+ "source": contract_dataset_name,
155
+ }])
156
+ count += 1
157
+ if count >= max_contracts:
158
+ break
159
+ except Exception as e:
160
+ print(f"Warning: could not load {contract_dataset_name}: {e}")
161
+ return retriever