Phoenix1410 commited on
Commit
cecd86b
·
verified ·
1 Parent(s): f6bf7e9

Initial commit

Browse files
Dockerfile ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 1. Use Python 3.9 Slim
2
+ FROM python:3.9-slim
3
+
4
+ # 2. Create a user (Hugging Face requirement)
5
+ # We cannot run as root. We must create a user with ID 1000.
6
+ RUN useradd -m -u 1000 user
7
+ USER user
8
+ ENV PATH="/home/user/.local/bin:$PATH"
9
+
10
+ # 3. Set work directory
11
+ WORKDIR /app
12
+
13
+ # 4. Install Dependencies
14
+ # Copy requirements first to leverage Docker cache
15
+ COPY --chown=user ./requirements.txt requirements.txt
16
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
17
+
18
+ # 5. Copy the Application Code
19
+ # Note: This will copy your model folder too, if it's in the directory
20
+ COPY --chown=user . .
21
+
22
+ # 6. Run the Application
23
+ # CRITICAL: Hugging Face expects Port 7860
24
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
lexguard_model/config.json ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "RobertaForSequenceClassification"
4
+ ],
5
+ "attention_probs_dropout_prob": 0.1,
6
+ "bos_token_id": 0,
7
+ "classifier_dropout": null,
8
+ "dtype": "float32",
9
+ "eos_token_id": 2,
10
+ "hidden_act": "gelu",
11
+ "hidden_dropout_prob": 0.1,
12
+ "hidden_size": 768,
13
+ "id2label": {
14
+ "0": "LABEL_0",
15
+ "1": "LABEL_1",
16
+ "2": "LABEL_2"
17
+ },
18
+ "initializer_range": 0.02,
19
+ "intermediate_size": 3072,
20
+ "label2id": {
21
+ "LABEL_0": 0,
22
+ "LABEL_1": 1,
23
+ "LABEL_2": 2
24
+ },
25
+ "layer_norm_eps": 1e-05,
26
+ "max_position_embeddings": 514,
27
+ "model_type": "roberta",
28
+ "num_attention_heads": 12,
29
+ "num_hidden_layers": 6,
30
+ "pad_token_id": 1,
31
+ "position_embedding_type": "absolute",
32
+ "problem_type": "single_label_classification",
33
+ "transformers_version": "4.57.3",
34
+ "type_vocab_size": 1,
35
+ "use_cache": true,
36
+ "vocab_size": 50265
37
+ }
lexguard_model/merges.txt ADDED
The diff for this file is too large to render. See raw diff
 
lexguard_model/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8c9d6c5959fc64b88fbd7076db3d72900d7e38717497d3949b6ee04513432ec0
3
+ size 328495356
lexguard_model/special_tokens_map.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "bos_token": "<s>",
3
+ "cls_token": "<s>",
4
+ "eos_token": "</s>",
5
+ "mask_token": {
6
+ "content": "<mask>",
7
+ "lstrip": true,
8
+ "normalized": false,
9
+ "rstrip": false,
10
+ "single_word": false
11
+ },
12
+ "pad_token": "<pad>",
13
+ "sep_token": "</s>",
14
+ "unk_token": "<unk>"
15
+ }
lexguard_model/tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
lexguard_model/tokenizer_config.json ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_prefix_space": false,
3
+ "added_tokens_decoder": {
4
+ "0": {
5
+ "content": "<s>",
6
+ "lstrip": false,
7
+ "normalized": true,
8
+ "rstrip": false,
9
+ "single_word": false,
10
+ "special": true
11
+ },
12
+ "1": {
13
+ "content": "<pad>",
14
+ "lstrip": false,
15
+ "normalized": true,
16
+ "rstrip": false,
17
+ "single_word": false,
18
+ "special": true
19
+ },
20
+ "2": {
21
+ "content": "</s>",
22
+ "lstrip": false,
23
+ "normalized": true,
24
+ "rstrip": false,
25
+ "single_word": false,
26
+ "special": true
27
+ },
28
+ "3": {
29
+ "content": "<unk>",
30
+ "lstrip": false,
31
+ "normalized": true,
32
+ "rstrip": false,
33
+ "single_word": false,
34
+ "special": true
35
+ },
36
+ "50264": {
37
+ "content": "<mask>",
38
+ "lstrip": true,
39
+ "normalized": false,
40
+ "rstrip": false,
41
+ "single_word": false,
42
+ "special": true
43
+ }
44
+ },
45
+ "bos_token": "<s>",
46
+ "clean_up_tokenization_spaces": false,
47
+ "cls_token": "<s>",
48
+ "eos_token": "</s>",
49
+ "errors": "replace",
50
+ "extra_special_tokens": {},
51
+ "mask_token": "<mask>",
52
+ "model_max_length": 512,
53
+ "pad_token": "<pad>",
54
+ "sep_token": "</s>",
55
+ "tokenizer_class": "RobertaTokenizer",
56
+ "trim_offsets": true,
57
+ "unk_token": "<unk>"
58
+ }
lexguard_model/vocab.json ADDED
The diff for this file is too large to render. See raw diff
 
main.py ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, UploadFile, File, Form, HTTPException
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from pydantic import BaseModel
4
+ from transformers import pipeline, AutoTokenizer, AutoModelForSequenceClassification
5
+ from langchain_groq import ChatGroq
6
+ from langchain_core.prompts import ChatPromptTemplate
7
+ import os
8
+ import jwt
9
+ import fitz # PyMuPDF
10
+ from dotenv import load_dotenv
11
+ from typing import Dict, Optional
12
+
13
+ # DB Imports
14
+ from models import UserSync
15
+ from database import db
16
+
17
+ # 1. Setup App & Configuration
18
+ load_dotenv()
19
+ app = FastAPI(title="LexGuard AI Core")
20
+
21
+ # --- CORS CONFIGURATION (CRITICAL FOR REACT) ---
22
+ app.add_middleware(
23
+ CORSMiddleware,
24
+ allow_origins=["*"], # Allows localhost:3000, localhost:5173, etc.
25
+ allow_credentials=True,
26
+ allow_methods=["*"],
27
+ allow_headers=["*"],
28
+ )
29
+
30
+ # --- MODEL PATHS ---
31
+ MODEL_DIR = "./lexguard_model"
32
+ ABS_MODEL_PATH = os.path.abspath(MODEL_DIR)
33
+ GROQ_API_KEY = os.getenv("GROQ_API_KEY")
34
+
35
+ # 2. Load Models (The Sniper & The Analyst)
36
+ print(f"Loading Predictive Model from: {ABS_MODEL_PATH}")
37
+ try:
38
+ # Force local loading to prevent Hugging Face URL errors
39
+ model = AutoModelForSequenceClassification.from_pretrained(
40
+ ABS_MODEL_PATH, local_files_only=True
41
+ )
42
+ tokenizer = AutoTokenizer.from_pretrained(
43
+ ABS_MODEL_PATH, local_files_only=True
44
+ )
45
+ # device=0 uses RTX 4060
46
+ classifier = pipeline("text-classification", model=model, tokenizer=tokenizer, device=0)
47
+ print("✅ Sniper Model Loaded on GPU")
48
+ except Exception as e:
49
+ print(f"❌ Error loading model: {e}")
50
+ exit()
51
+
52
+ llm = ChatGroq(
53
+ temperature=0,
54
+ model_name="llama-3.3-70b-versatile",
55
+ groq_api_key=GROQ_API_KEY
56
+ )
57
+
58
+ # 3. Helper: PDF to Text Chunks
59
+ def extract_text_from_pdf(file_bytes):
60
+ """
61
+ Opens PDF bytes, extracts text, and splits into paragraphs (clauses).
62
+ """
63
+ doc = fitz.open(stream=file_bytes, filetype="pdf")
64
+ text_chunks = []
65
+
66
+ for page in doc:
67
+ text = page.get_text("text")
68
+ # Split by double newline to find paragraphs
69
+ paragraphs = text.split('\n\n')
70
+ for p in paragraphs:
71
+ clean_p = " ".join(p.split()).strip()
72
+ # Filter out tiny page numbers or headers
73
+ if len(clean_p) > 50:
74
+ text_chunks.append(clean_p)
75
+ return text_chunks
76
+
77
+ # 4. API Endpoints
78
+
79
+ @app.get("/")
80
+ def health_check():
81
+ return {"status": "LexGuard API is Ready", "gpu_active": True}
82
+
83
+ @app.post("/users/sync")
84
+ async def sync_user(user: UserSync):
85
+ """
86
+ Syncs user from Clerk Frontend to MongoDB Backend.
87
+ """
88
+ user_data = user.model_dump()
89
+ # Upsert based on clerk_id
90
+ result = await db.users.update_one(
91
+ {"clerk_id": user.clerk_id},
92
+ {"$set": user_data},
93
+ upsert=True
94
+ )
95
+ return {"status": "synced", "updated": result.modified_count > 0 or result.upserted_id is not None}
96
+
97
+ @app.post("/analyze_document")
98
+ async def analyze_document(
99
+ file: UploadFile = File(...),
100
+ user_rule: str = Form(None) # React sends this as FormData
101
+ ):
102
+ """
103
+ Main Endpoint: Receives a PDF file + Optional User Rule.
104
+ Returns a list of Risky Clauses with GenAI explanations.
105
+ """
106
+ print(f"📥 Receiving file: {file.filename}")
107
+
108
+ # A. Read & Parse PDF
109
+ try:
110
+ content = await file.read()
111
+ clauses = extract_text_from_pdf(content)
112
+ except Exception as e:
113
+ raise HTTPException(status_code=400, detail=f"Invalid PDF: {str(e)}")
114
+
115
+ results = []
116
+
117
+ # B. The Sniper Pass (Batch Prediction)
118
+ # We predict all clauses at once for speed
119
+ print(f"🔍 Scanning {len(clauses)} clauses...")
120
+
121
+ # Map model output to human labels
122
+ label_map = {"LABEL_0": "Safe", "LABEL_1": "Termination", "LABEL_2": "Non-Compete"}
123
+
124
+ # Run inference (This is fast on RTX 4060)
125
+ predictions = classifier(clauses, batch_size=8, truncation=True)
126
+
127
+ # C. The Filter & Logic Pass
128
+ for i, (clause, pred) in enumerate(zip(clauses, predictions)):
129
+ label_str = pred['label']
130
+ score = pred['score']
131
+ risk_type = label_map.get(label_str, "Safe")
132
+
133
+ # LOGIC: We only keep it if it's RISKY OR if User has a specific rule to check
134
+ # (We skip "Safe" clauses to save GenAI tokens and Frontend clutter)
135
+ is_risky = risk_type != "Safe"
136
+ has_rule = user_rule is not None and len(user_rule) > 5
137
+
138
+ if is_risky or has_rule:
139
+
140
+ explanation = "Standard standard clause."
141
+
142
+ # D. The Analyst Pass (GenAI)
143
+ # Only call GenAI if it's actually risky or we need to check a rule
144
+ # If it's Safe but we have a rule, we ask GenAI to check that rule specifically
145
+ if is_risky or (has_rule and i < 20): # Limit rule checking to first 20 clauses for speed
146
+
147
+ system_msg = f"""
148
+ You are a legal auditor.
149
+ Detected Risk Category: {risk_type} (Confidence: {score:.2f})
150
+ User's Constraint Rule: {user_rule if user_rule else "None"}
151
+
152
+ Task:
153
+ 1. Summarize what this clause says in plain English.
154
+ 2. If a User Rule exists, explicitly state if this clause violates it.
155
+ 3. If it is risky, suggest a 1-sentence edit to make it safer.
156
+ """
157
+
158
+ prompt = ChatPromptTemplate.from_messages([
159
+ ("system", system_msg),
160
+ ("human", clause)
161
+ ])
162
+
163
+ try:
164
+ # Invoke Groq
165
+ ai_response = (prompt | llm).invoke({})
166
+ explanation = ai_response.content
167
+ except Exception as e:
168
+ explanation = "AI Analysis unavailable."
169
+
170
+ # Append to results
171
+ results.append({
172
+ "id": i,
173
+ "text": clause,
174
+ "risk_type": risk_type,
175
+ "confidence": round(score, 4),
176
+ "explanation": explanation
177
+ })
178
+
179
+ return {
180
+ "filename": file.filename,
181
+ "total_clauses_scanned": len(clauses),
182
+ "risks_found": len(results),
183
+ "results": results
184
+ }
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ pydantic
4
+ transformers
5
+ langchain-groq
6
+ langchain-core
7
+ PyMuPDF
8
+ python-dotenv
9
+ pyjwt
10
+ motor
11
+ torch
12
+ accelerate # Optional, usually needed for transformers on some setups