yamraj047 commited on
Commit
25f61c4
·
verified ·
1 Parent(s): 27698b5

Upload 5 files

Browse files
Files changed (5) hide show
  1. Dockerfile.txt +43 -0
  2. app.py +342 -0
  3. gitattributes.txt +35 -0
  4. gitignore.txt +13 -0
  5. requirements.txt +9 -0
Dockerfile.txt ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use Python 3.10 slim image
2
+ FROM python:3.10-slim
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Set environment variables
8
+ ENV PYTHONUNBUFFERED=1 \
9
+ PYTHONDONTWRITEBYTECODE=1 \
10
+ PIP_NO_CACHE_DIR=1 \
11
+ PIP_DISABLE_PIP_VERSION_CHECK=1
12
+
13
+ # Install system dependencies (minimal)
14
+ RUN apt-get update && apt-get install -y --no-install-recommends \
15
+ build-essential \
16
+ curl \
17
+ && rm -rf /var/lib/apt/lists/* \
18
+ && apt-get clean
19
+
20
+ # Copy requirements
21
+ COPY requirements.txt .
22
+
23
+ # Install Python packages with optimizations
24
+ RUN pip install --no-cache-dir --upgrade pip && \
25
+ pip install --no-cache-dir -r requirements.txt
26
+
27
+ # Copy application files
28
+ COPY app.py .
29
+ COPY final_legal_embeddings.npy .
30
+ COPY final_legal_laws_metadata.json .
31
+
32
+ # Create cache directory for models
33
+ RUN mkdir -p /root/.cache/huggingface
34
+
35
+ # Expose port
36
+ EXPOSE 7860
37
+
38
+ # Health check
39
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
40
+ CMD curl --fail http://localhost:7860/health || exit 1
41
+
42
+ # Run the application
43
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1"]
app.py ADDED
@@ -0,0 +1,342 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from pydantic import BaseModel
4
+ import numpy as np
5
+ import json
6
+ import faiss
7
+ import re
8
+ from sentence_transformers import SentenceTransformer, CrossEncoder
9
+ from groq import Groq
10
+ import os
11
+ from typing import List, Dict, Optional
12
+ import logging
13
+ import httpx
14
+
15
+ logging.basicConfig(level=logging.INFO)
16
+ logger = logging.getLogger(__name__)
17
+
18
+ app = FastAPI(
19
+ title="LexNepal AI API",
20
+ description="Advanced Legal Intelligence API for Nepal Legal Code",
21
+ version="1.0.0",
22
+ docs_url="/",
23
+ redoc_url="/redoc"
24
+ )
25
+
26
+ app.add_middleware(
27
+ CORSMiddleware,
28
+ allow_origins=["*"],
29
+ allow_credentials=True,
30
+ allow_methods=["*"],
31
+ allow_headers=["*"],
32
+ )
33
+
34
+ class QueryRequest(BaseModel):
35
+ query: str
36
+ max_sources: Optional[int] = 10
37
+
38
+ class Source(BaseModel):
39
+ law: str
40
+ section: str
41
+ section_title: str
42
+ text: str
43
+ rel_score: float
44
+
45
+ class QueryResponse(BaseModel):
46
+ answer: str
47
+ sources: List[Source]
48
+ query: str
49
+ total_candidates: int
50
+
51
+ class StatsResponse(BaseModel):
52
+ total_provisions: int
53
+ total_laws: int
54
+ vector_dimensions: int
55
+ embedding_model: str
56
+ reranking_model: str
57
+ llm_model: str
58
+
59
+ class HealthResponse(BaseModel):
60
+ status: str
61
+ models_loaded: bool
62
+ message: Optional[str] = None
63
+
64
+ _bi_encoder = None
65
+ _cross_encoder = None
66
+ _groq_client = None
67
+ _index = None
68
+ _metadata = None
69
+
70
+ def get_bi_encoder():
71
+ global _bi_encoder
72
+ if _bi_encoder is None:
73
+ logger.info("Loading bi-encoder (MPNet)...")
74
+ _bi_encoder = SentenceTransformer("all-mpnet-base-v2")
75
+ logger.info("✅ Bi-encoder loaded successfully")
76
+ return _bi_encoder
77
+
78
+ def get_cross_encoder():
79
+ global _cross_encoder
80
+ if _cross_encoder is None:
81
+ logger.info("Loading cross-encoder...")
82
+ _cross_encoder = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
83
+ logger.info("✅ Cross-encoder loaded successfully")
84
+ return _cross_encoder
85
+
86
+ def get_groq_client():
87
+ global _groq_client
88
+ if _groq_client is None:
89
+ logger.info("Initializing Groq client...")
90
+ groq_api_key = os.getenv("GROQ_API_KEY", "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")
91
+
92
+ try:
93
+ # Try standard initialization
94
+ _groq_client = Groq(api_key=groq_api_key)
95
+ logger.info("✅ Groq client initialized (standard)")
96
+ except TypeError as e:
97
+ logger.warning(f"Standard Groq init failed: {e}, trying with custom HTTP client...")
98
+ try:
99
+ # Fallback with custom HTTP client
100
+ http_client = httpx.Client(timeout=60.0)
101
+ _groq_client = Groq(
102
+ api_key=groq_api_key,
103
+ http_client=http_client
104
+ )
105
+ logger.info("✅ Groq client initialized (with custom HTTP client)")
106
+ except Exception as e2:
107
+ logger.error(f"❌ Failed to initialize Groq client: {e2}")
108
+ raise HTTPException(
109
+ status_code=503,
110
+ detail=f"Failed to initialize Groq client: {str(e2)}"
111
+ )
112
+
113
+ return _groq_client
114
+
115
+ def get_index():
116
+ global _index
117
+ if _index is None:
118
+ logger.info("Loading embeddings and creating FAISS index...")
119
+ try:
120
+ embeddings = np.load("final_legal_embeddings.npy")
121
+ logger.info(f"Embeddings shape: {embeddings.shape}")
122
+ _index = faiss.IndexFlatL2(embeddings.shape[1])
123
+ _index.add(embeddings.astype('float32'))
124
+ logger.info(f"✅ FAISS index created with {embeddings.shape[0]} vectors")
125
+ except FileNotFoundError:
126
+ logger.error("❌ Embeddings file not found")
127
+ raise HTTPException(
128
+ status_code=503,
129
+ detail="Embeddings file not found. Please upload final_legal_embeddings.npy"
130
+ )
131
+ return _index
132
+
133
+ def get_metadata():
134
+ global _metadata
135
+ if _metadata is None:
136
+ logger.info("Loading metadata...")
137
+ try:
138
+ with open("final_legal_laws_metadata.json", "r", encoding="utf-8") as f:
139
+ _metadata = json.load(f)
140
+ logger.info(f"✅ Loaded {len(_metadata)} legal provisions")
141
+ except FileNotFoundError:
142
+ logger.error("❌ Metadata file not found")
143
+ raise HTTPException(
144
+ status_code=503,
145
+ detail="Metadata file not found. Please upload final_legal_laws_metadata.json"
146
+ )
147
+ return _metadata
148
+
149
+ def get_premium_context(query: str, max_sources: int = 10) -> List[Dict]:
150
+ try:
151
+ bi_encoder = get_bi_encoder()
152
+ cross_encoder = get_cross_encoder()
153
+ index = get_index()
154
+ metadata = get_metadata()
155
+
156
+ # Stage 1: Encode query
157
+ query_embedding = bi_encoder.encode([query], convert_to_numpy=True)
158
+
159
+ # Stage 2: Dense retrieval
160
+ _, indices = index.search(query_embedding.astype('float32'), 25)
161
+
162
+ candidates = []
163
+ seen = set()
164
+
165
+ for i in indices[0]:
166
+ if i != -1 and i < len(metadata):
167
+ candidates.append(metadata[i].copy())
168
+ seen.add(i)
169
+
170
+ # Stage 3: Keyword boosting
171
+ numbers = re.findall(r'\d+', query)
172
+ if numbers:
173
+ for i, item in enumerate(metadata):
174
+ if any(str(item.get('section', '')) == n for n in numbers):
175
+ if i not in seen:
176
+ candidates.append(item.copy())
177
+ seen.add(i)
178
+
179
+ # Stage 4: Cross-encoder reranking
180
+ if candidates:
181
+ pairs = [
182
+ [query, f"{c.get('law', '')} {c.get('section_title', '')} {c.get('text', '')}"]
183
+ for c in candidates
184
+ ]
185
+ scores = cross_encoder.predict(pairs)
186
+
187
+ for i, c in enumerate(candidates):
188
+ c['rel_score'] = float(scores[i])
189
+
190
+ candidates = sorted(candidates, key=lambda x: x['rel_score'], reverse=True)[:max_sources]
191
+
192
+ logger.info(f"Retrieved {len(candidates)} relevant candidates")
193
+ return candidates
194
+
195
+ except Exception as e:
196
+ logger.error(f"Error in context retrieval: {str(e)}")
197
+ raise HTTPException(status_code=500, detail=f"Context retrieval error: {str(e)}")
198
+
199
+ @app.get("/health", response_model=HealthResponse)
200
+ async def health_check():
201
+ """Health check endpoint"""
202
+ try:
203
+ metadata = get_metadata()
204
+ models_loaded = True
205
+ message = f"API is healthy. {len(metadata)} provisions loaded."
206
+ except Exception as e:
207
+ models_loaded = False
208
+ message = f"Error: {str(e)}"
209
+
210
+ return {
211
+ "status": "healthy" if models_loaded else "unhealthy",
212
+ "models_loaded": models_loaded,
213
+ "message": message
214
+ }
215
+
216
+ @app.get("/stats", response_model=StatsResponse)
217
+ async def get_statistics():
218
+ """Get database statistics"""
219
+ try:
220
+ metadata = get_metadata()
221
+ unique_laws = len(set(d.get('law', '') for d in metadata))
222
+
223
+ return {
224
+ "total_provisions": len(metadata),
225
+ "total_laws": unique_laws,
226
+ "vector_dimensions": 768,
227
+ "embedding_model": "all-mpnet-base-v2",
228
+ "reranking_model": "ms-marco-MiniLM-L-6-v2",
229
+ "llm_model": "llama-3.3-70b-versatile"
230
+ }
231
+ except Exception as e:
232
+ logger.error(f"Error getting stats: {str(e)}")
233
+ raise HTTPException(status_code=503, detail=str(e))
234
+
235
+ @app.post("/query", response_model=QueryResponse)
236
+ async def process_legal_query(request: QueryRequest):
237
+ """Process legal query with RAG pipeline"""
238
+
239
+ # Validation
240
+ if not request.query.strip():
241
+ raise HTTPException(status_code=400, detail="Query cannot be empty")
242
+
243
+ if len(request.query) > 1000:
244
+ raise HTTPException(status_code=400, detail="Query too long (max 1000 characters)")
245
+
246
+ try:
247
+ logger.info(f"Processing query: {request.query[:100]}...")
248
+
249
+ # Get relevant context
250
+ candidates = get_premium_context(request.query, request.max_sources)
251
+
252
+ if not candidates:
253
+ return {
254
+ "answer": "No relevant legal provisions found in the database for your query. Please try rephrasing or consult a legal professional.",
255
+ "sources": [],
256
+ "query": request.query,
257
+ "total_candidates": 0
258
+ }
259
+
260
+ # Build context string
261
+ context_str = "\n\n".join([
262
+ f"[{d['law']} Section {d['section']}]: {d['text']}"
263
+ for d in candidates
264
+ ])
265
+
266
+ # System prompt
267
+ system_prompt = """You are an Elite Legal Advisor specializing in Nepal law.
268
+
269
+ OPERATIONAL MANDATE:
270
+ 1. Answer STRICTLY from provided legal text
271
+ 2. If information is absent, state: "No specific provision found in current database"
272
+ 3. Always cite exact Law name and Section number
273
+ 4. Use formal, authoritative legal language
274
+ 5. NEVER hallucinate or infer beyond provided text
275
+ 6. Maintain zero-tolerance policy for speculation
276
+
277
+ When citing, use format: "According to [Law Name], Section [Number]..."
278
+ Provide clear, structured answers with proper legal citations."""
279
+
280
+ # Generate response using Groq
281
+ logger.info("Generating LLM response...")
282
+ groq_client = get_groq_client()
283
+
284
+ response = groq_client.chat.completions.create(
285
+ model="llama-3.3-70b-versatile",
286
+ messages=[
287
+ {"role": "system", "content": system_prompt},
288
+ {"role": "user", "content": f"Legal Context:\n{context_str}\n\nQuery: {request.query}"}
289
+ ],
290
+ temperature=0,
291
+ max_tokens=1500
292
+ )
293
+
294
+ answer = response.choices[0].message.content
295
+
296
+ # Format sources
297
+ sources = [
298
+ Source(
299
+ law=d['law'],
300
+ section=str(d['section']),
301
+ section_title=d['section_title'],
302
+ text=d['text'],
303
+ rel_score=d['rel_score']
304
+ )
305
+ for d in candidates
306
+ ]
307
+
308
+ logger.info(f"✅ Query processed successfully with {len(sources)} sources")
309
+
310
+ return {
311
+ "answer": answer,
312
+ "sources": sources,
313
+ "query": request.query,
314
+ "total_candidates": len(candidates)
315
+ }
316
+
317
+ except HTTPException:
318
+ raise
319
+ except Exception as e:
320
+ logger.error(f"Error processing query: {str(e)}")
321
+ raise HTTPException(status_code=500, detail=f"Query processing error: {str(e)}")
322
+
323
+ @app.get("/")
324
+ async def root():
325
+ """Root endpoint - API info"""
326
+ return {
327
+ "message": "🇳🇵 LexNepal AI API is running",
328
+ "version": "1.0.0",
329
+ "description": "Advanced Legal Intelligence for Nepal Legal Code",
330
+ "endpoints": {
331
+ "docs": "/ (Swagger UI)",
332
+ "health": "/health (GET)",
333
+ "stats": "/stats (GET)",
334
+ "query": "/query (POST)"
335
+ },
336
+ "technology": "RAG with Hybrid Retrieval + Cross-Encoder Reranking",
337
+ "support": "https://huggingface.co/spaces/yamraj047/lexnepal-api"
338
+ }
339
+
340
+ if __name__ == "__main__":
341
+ import uvicorn
342
+ uvicorn.run(app, host="0.0.0.0", port=7860)
gitattributes.txt ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
gitignore.txt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.so
5
+ .Python
6
+ env/
7
+ venv/
8
+ ENV/
9
+ .venv
10
+ *.log
11
+ .DS_Store
12
+ .idea/
13
+ .vscode/
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.109.0
2
+ uvicorn[standard]==0.27.0
3
+ sentence-transformers==2.3.1
4
+ faiss-cpu==1.7.4
5
+ groq==0.11.0
6
+ numpy==1.24.3
7
+ pydantic==2.5.3
8
+ python-multipart==0.0.6
9
+ httpx==0.27.0