Spaces:
Sleeping
Sleeping
| """ | |
| Compliance Copilot -- Phase 1: Data Preparation | |
| ================================================ | |
| Goal of this file (one sentence): | |
| Take a big public dataset of real contract clauses and turn it into small, | |
| clean files we can feed to our model in Phase 2. | |
| Why LEDGAR? | |
| LEDGAR is a public dataset of contract provisions ("clauses"), where each | |
| clause is labelled with its type (e.g. "Confidentiality", "Governing Laws", | |
| "Terminations"). This is exactly the "what kind of clause is this / is it | |
| compliant" problem a compliance team faces every day. It is public and | |
| license-friendly, so we can safely showcase it (unlike private company data). | |
| Where to run this: | |
| In Google Colab (free) or on your own laptop. It needs internet the first | |
| time so it can download the dataset from the Hugging Face Hub. | |
| Install the one thing it needs: | |
| pip install -U datasets | |
| Output (all written into this ./data folder): | |
| labels.json -> the list of clause types we keep | |
| train.jsonl -> training examples (chat format, used to teach the model) | |
| val.jsonl -> validation examples (chat format, used to watch progress) | |
| test.jsonl -> held-out examples (raw text + label, used to score accuracy) | |
| """ | |
| import json | |
| import random | |
| from collections import Counter | |
| from pathlib import Path | |
| from datasets import load_dataset | |
| # --------------------------------------------------------------------------- | |
| # Settings you are allowed to play with | |
| # --------------------------------------------------------------------------- | |
| TOP_K_LABELS = 10 # keep only the K most common clause types | |
| MAX_PER_LABEL_TRAIN = 400 # cap training examples per label (keeps training fast + free) | |
| MAX_PER_LABEL_EVAL = 60 # cap validation/test examples per label | |
| SEED = 42 | |
| OUT_DIR = Path(__file__).resolve().parent # writes next to this script (the data/ folder) | |
| # This is the "job description" we hand the model. IMPORTANT: the SAME text must | |
| # be used again when we serve the model in Phase 3, otherwise it gets confused. | |
| # That is why we keep it here in one place and reuse it everywhere. | |
| SYSTEM_PROMPT = ( | |
| "You are a compliance assistant. You read a single contract clause and reply " | |
| "with the one clause type it belongs to, and nothing else." | |
| ) | |
| def build_user_prompt(clause_text: str, labels: list) -> str: | |
| """Turn a clause + the menu of allowed answers into the question we ask the model.""" | |
| label_menu = ", ".join(labels) | |
| return ( | |
| f"Classify the following contract clause into exactly one of these types: " | |
| f"{label_menu}.\n\n" | |
| f"Clause:\n{clause_text}\n\n" | |
| f"Clause type:" | |
| ) | |
| # --------------------------------------------------------------------------- | |
| random.seed(SEED) | |
| def main() -> None: | |
| OUT_DIR.mkdir(parents=True, exist_ok=True) | |
| print("Step 1/5 Downloading LEDGAR from the Hugging Face Hub ...") | |
| ds = load_dataset("coastalcph/lex_glue", "ledgar") | |
| # The dataset stores labels as numbers (0, 1, 2, ...). This list turns a | |
| # number back into a human name, e.g. 12 -> "Confidentiality". | |
| label_names = ds["train"].features["label"].names | |
| print(f" The full dataset has {len(label_names)} clause types.") | |
| print("Step 2/5 Finding the most common clause types ...") | |
| counts = Counter(ds["train"]["label"]) # how often each label appears | |
| top_ids = [label_id for label_id, _ in counts.most_common(TOP_K_LABELS)] | |
| kept_labels = [label_names[i] for i in top_ids] # the names we keep | |
| id_to_name = {i: label_names[i] for i in top_ids} | |
| print(f" Keeping the top {TOP_K_LABELS}: {kept_labels}") | |
| # Save the label list so every other part of the project agrees on it. | |
| (OUT_DIR / "labels.json").write_text(json.dumps(kept_labels, indent=2)) | |
| def collect(split_name: str, cap_per_label: int): | |
| """Grab up to cap_per_label examples for each label so classes stay balanced.""" | |
| buckets = {i: [] for i in top_ids} | |
| for row in ds[split_name]: | |
| lid = row["label"] | |
| if lid in buckets and len(buckets[lid]) < cap_per_label: | |
| buckets[lid].append(row["text"]) | |
| pairs = [(text, id_to_name[lid]) for lid, texts in buckets.items() for text in texts] | |
| random.shuffle(pairs) | |
| return pairs | |
| print("Step 3/5 Selecting + balancing examples ...") | |
| train_pairs = collect("train", MAX_PER_LABEL_TRAIN) | |
| val_pairs = collect("validation", MAX_PER_LABEL_EVAL) | |
| test_pairs = collect("test", MAX_PER_LABEL_EVAL) | |
| print(f" train={len(train_pairs)} val={len(val_pairs)} test={len(test_pairs)}") | |
| print("Step 4/5 Writing training + validation files (chat format) ...") | |
| def write_chat(path: Path, pairs): | |
| with path.open("w") as f: | |
| for text, label in pairs: | |
| example = { | |
| "messages": [ | |
| {"role": "system", "content": SYSTEM_PROMPT}, | |
| {"role": "user", "content": build_user_prompt(text, kept_labels)}, | |
| {"role": "assistant", "content": label}, | |
| ] | |
| } | |
| f.write(json.dumps(example) + "\n") | |
| write_chat(OUT_DIR / "train.jsonl", train_pairs) | |
| write_chat(OUT_DIR / "val.jsonl", val_pairs) | |
| print("Step 5/5 Writing the test file (raw text + label, for scoring) ...") | |
| with (OUT_DIR / "test.jsonl").open("w") as f: | |
| for text, label in test_pairs: | |
| f.write(json.dumps({"text": text, "label": label}) + "\n") | |
| print("\nDone. Files written into:", OUT_DIR) | |
| print("Next: open the Phase 2 fine-tuning notebook.") | |
| if __name__ == "__main__": | |
| main() | |