singhankur01 commited on
Commit
8ca2283
·
verified ·
1 Parent(s): 458c72f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +194 -98
app.py CHANGED
@@ -1,90 +1,103 @@
 
1
  import os
2
  import json
3
  import re
4
  import asyncio
5
- import time
6
- import numpy as np
7
- from cachetools import TTLCache
8
  from contextlib import asynccontextmanager
9
  from dotenv import load_dotenv
10
  from operator import itemgetter
 
 
11
  from fastapi import FastAPI, Depends, HTTPException, Header
12
  from fastapi.responses import JSONResponse
13
- from langchain_core.documents import Document
14
  from utils.DocsLoader import load_and_chunk
15
  from utils.Schemas import RunRequest, RunResponse
 
16
  from langchain_community.vectorstores import FAISS
 
17
  from langchain_google_genai import ChatGoogleGenerativeAI
18
  from langchain_huggingface import HuggingFaceEmbeddings
 
19
  from langchain_community.retrievers import BM25Retriever
20
  from langchain.retrievers import EnsembleRetriever
21
  from sklearn.metrics.pairwise import cosine_similarity
 
 
 
 
 
 
22
  from langchain.prompts import PromptTemplate
23
- from fastapi.middleware.cors import CORSMiddleware # Optional, for external frontend
24
 
25
  # Load environment variables
26
  load_dotenv()
27
 
28
- # Cache setup
29
- document_cache = TTLCache(maxsize=5, ttl=300)
30
- cache_lock = asyncio.Lock()
31
-
32
- # --- 1. Lifespan Event Handler ---
33
  ml_models = {}
34
 
35
  @asynccontextmanager
36
  async def lifespan(app: FastAPI):
 
37
  print("🚀 Initializing models and prompt template...")
38
 
39
  try:
40
  GOOGLE_API_KEY = os.getenv("gemini_api_key")
 
 
41
  if not GOOGLE_API_KEY:
42
  raise RuntimeError("CRITICAL: Missing GOOGLE_API_KEY in environment secrets!")
43
 
44
- ml_models["embedder"] = HuggingFaceEmbeddings(
45
- model_name="BAAI/bge-base-en-v1.5",
46
- encode_kwargs={
47
- 'batch_size': 64,
48
- 'show_progress_bar': False
49
- }
50
- )
51
-
 
 
 
 
52
  ml_models["llm"] = ChatGoogleGenerativeAI(
53
- model="gemini-1.5-pro",
54
- api_key=GOOGLE_API_KEY,
55
- temperature=0.1,
56
- max_output_tokens=300
57
- )
58
-
59
  ml_models["prompt_template"] = PromptTemplate.from_template("""
60
- **Role**: Insurance Policy Expert
 
61
  **Context**:
62
  {context}
63
 
64
  **Query**: {full_query}
65
 
66
  **Instructions**:
67
- 1. If query contains age/gender/procedure/location/duration:
68
  - Output ONLY JSON: {{"decision":"approved/rejected","amount":"₹X","justification":"Clause reference"}}
69
  2. Else: Provide concise answer
70
  3. NEVER mention document sources
71
  4. If unsure, respond: "Insufficient information"
72
 
73
  **Response**:
74
- """)
 
75
  print("✅ Models and prompt loaded successfully!")
76
  except Exception as e:
77
- print(f"❌ Lifespan error: {str(e)}")
78
  raise e
79
 
80
  yield
81
  print("🧹 Cleaning up.")
82
  ml_models.clear()
83
-
84
  # --- 2. FastAPI App Instance ---
 
85
  app = FastAPI(title="HackRX RAG Server", lifespan=lifespan)
86
 
87
-
88
  # --- 3. API Key Verification ---
89
  TEAM_API_KEY = os.getenv("TEAM_API_KEY")
90
 
@@ -92,95 +105,178 @@ def verify_api_key(authorization: str = Header(...)):
92
  if not authorization.startswith("Bearer "):
93
  raise HTTPException(status_code=401, detail="Invalid Authorization header format")
94
  token = authorization.split("Bearer ")[1]
 
 
95
  if token != TEAM_API_KEY:
96
  raise HTTPException(status_code=403, detail="Invalid or missing API key")
97
 
 
98
  # --- 4. Parsing Helper ---
99
  def parse_llm_response(content: str) -> str:
100
  try:
 
101
  content_cleaned = re.sub(r"^```json|```$", "", content.strip(), flags=re.IGNORECASE).strip()
102
  data = json.loads(content_cleaned)
103
-
104
- if "decision" in data:
105
- return (
106
- f"Decision: {data.get('decision', 'N/A').upper()}\n"
107
- f"Amount: {data.get('amount', 'Not specified')}\n"
108
- f"Justification: {data.get('justification', 'No justification provided')}"
109
- )
110
- elif "response" in data:
111
- return data["response"]
112
- return "Response format error"
 
 
 
113
  except json.JSONDecodeError:
114
- return content.strip()
 
115
  except Exception as e:
116
- return f"Response processing error: {str(e)}"
117
-
118
- # --- 5. Retrieval Optimization ---
119
- async def get_relevant_docs(question: str, vectorstore: FAISS, keyword_retriever: BM25Retriever):
120
- dense_docs, sparse_docs = await asyncio.gather(
121
- vectorstore.asimilarity_search(question, k=6),
122
- asyncio.get_event_loop().run_in_executor(
123
- None,
124
- keyword_retriever.get_relevant_documents,
125
- question
126
- )
127
- )
128
-
129
- all_docs = dense_docs + sparse_docs
130
- unique_docs = {doc.page_content: doc for doc in all_docs}.values()
131
-
132
- query_embedding = ml_models["embedder"].embed_query(question)
133
- doc_texts = [doc.page_content for doc in unique_docs]
134
- doc_embeddings = ml_models["embedder"].embed_documents(doc_texts)
135
-
136
- similarities = cosine_similarity([query_embedding], doc_embeddings)[0]
137
- sorted_indices = np.argsort(similarities)[::-1][:5] # Top 5
138
-
139
- return [list(unique_docs)[i] for i in sorted_indices]
140
 
141
- # --- 6. Main API Endpoint ---
142
  @app.post("/api/v1/hackrx/run", response_model=RunResponse, dependencies=[Depends(verify_api_key)])
143
  async def run_hackrx(req: RunRequest):
144
- start_time = time.time()
145
-
146
- async with cache_lock:
147
- doc_key = hash(str(req.documents)) # Hash key for safety
148
- if doc_key in document_cache:
149
- print("♻️ Using cached document")
150
- chunks = document_cache[doc_key]
151
- else:
152
- raw_chunks = load_and_chunk(str(req.documents))
153
- # 🔧 FIXED: Convert string chunks to Document objects
154
- chunks = [Document(page_content=chunk) for chunk in raw_chunks]
155
- document_cache[doc_key] = chunks
156
-
157
  if not chunks:
158
- return JSONResponse({"error": "No documents processed"}, status_code=400)
 
 
 
 
 
 
 
 
 
 
 
 
159
 
160
- # Create vector store and retrievers
161
- vectorstore = await FAISS.afrom_documents(chunks, ml_models["embedder"])
162
  keyword_retriever = BM25Retriever.from_documents(chunks)
163
- keyword_retriever.k = 6
 
 
 
 
 
 
164
 
165
- # Process questions in parallel
166
- async def process_question(q: str):
167
- relevant_docs = await get_relevant_docs(q, vectorstore, keyword_retriever)
168
- context = "\n".join([d.page_content for d in relevant_docs])
169
-
170
- prompt = ml_models["prompt_template"].format_prompt(
171
- full_query=q,
172
- context=context
173
- )
174
- result = await ml_models["llm"].ainvoke(prompt)
175
- return parse_llm_response(result.content)
176
 
177
- answers = await asyncio.gather(*(process_question(q) for q in req.questions))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
 
179
- proc_time = time.time() - start_time
180
- print(f"⏱️ Processed {len(req.questions)} questions in {proc_time:.2f}s")
181
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  return JSONResponse({"answers": answers}, status_code=200)
183
 
184
  @app.get("/", include_in_schema=False)
185
  def root():
186
- return {"message": "HackRX API operational. Use /api/v1/hackrx/run"}
 
 
 
 
 
1
+
2
  import os
3
  import json
4
  import re
5
  import asyncio
 
 
 
6
  from contextlib import asynccontextmanager
7
  from dotenv import load_dotenv
8
  from operator import itemgetter
9
+
10
+ # import gradio as gr
11
  from fastapi import FastAPI, Depends, HTTPException, Header
12
  from fastapi.responses import JSONResponse
13
+
14
  from utils.DocsLoader import load_and_chunk
15
  from utils.Schemas import RunRequest, RunResponse
16
+ # from concurrent.futures import ThreadPoolExecutor
17
  from langchain_community.vectorstores import FAISS
18
+ from langchain.schema import Document
19
  from langchain_google_genai import ChatGoogleGenerativeAI
20
  from langchain_huggingface import HuggingFaceEmbeddings
21
+ # from langchain_chroma import Chroma
22
  from langchain_community.retrievers import BM25Retriever
23
  from langchain.retrievers import EnsembleRetriever
24
  from sklearn.metrics.pairwise import cosine_similarity
25
+ import numpy as np
26
+
27
+ ### to make it faster we are now using our built reranker thats why commenting the imports below
28
+ from langchain.retrievers import ContextualCompressionRetriever
29
+ from langchain.retrievers.document_compressors import CrossEncoderReranker
30
+ from langchain_community.cross_encoders import HuggingFaceCrossEncoder
31
  from langchain.prompts import PromptTemplate
32
+
33
 
34
  # Load environment variables
35
  load_dotenv()
36
 
37
+ # --- 1. Lifespan Event Handler (The New, Correct Way) ---
38
+ # This dictionary will hold our loaded models
 
 
 
39
  ml_models = {}
40
 
41
  @asynccontextmanager
42
  async def lifespan(app: FastAPI):
43
+ # This code runs ONCE when the application starts up
44
  print("🚀 Initializing models and prompt template...")
45
 
46
  try:
47
  GOOGLE_API_KEY = os.getenv("gemini_api_key")
48
+ print("🔑 gemini_api_key:", "FOUND" if GOOGLE_API_KEY else "NOT FOUND")
49
+
50
  if not GOOGLE_API_KEY:
51
  raise RuntimeError("CRITICAL: Missing GOOGLE_API_KEY in environment secrets!")
52
 
53
+ # Load models into the shared dictionary
54
+ ml_models["embedder"] = HuggingFaceEmbeddings(model_name="BAAI/bge-base-en-v1.5",
55
+ encode_kwargs={
56
+ "batch_size": 64
57
+ "normalize_embeddings": True
58
+ }
59
+ show_progress_bar= False
60
+ )
61
+ ### to make it faster we are now using our built reranker thats why commenting the code below
62
+ cross_encoder_model = HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-base")
63
+ # cross_encoder_model = HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-large")
64
+ ml_models["reranker_compressor"] = CrossEncoderReranker(model=cross_encoder_model, top_n=5)
65
  ml_models["llm"] = ChatGoogleGenerativeAI(
66
+ model="gemini-1.5-pro",
67
+ api_key=GOOGLE_API_KEY,
68
+ temperature=0.1,
69
+ max_output_tokens=300
70
+ )
 
71
  ml_models["prompt_template"] = PromptTemplate.from_template("""
72
+ **Role**: You are an expert decision maker Assistant in the domain such as insurance, legal compliance, human resources, and contract management.
73
+
74
  **Context**:
75
  {context}
76
 
77
  **Query**: {full_query}
78
 
79
  **Instructions**:
80
+ 1. If query contains age ,gender,procedure,duration , location ,and query like can i get a knee surgery ,if i am male and duration is 3months or any query is similar to the previus example :
81
  - Output ONLY JSON: {{"decision":"approved/rejected","amount":"₹X","justification":"Clause reference"}}
82
  2. Else: Provide concise answer
83
  3. NEVER mention document sources
84
  4. If unsure, respond: "Insufficient information"
85
 
86
  **Response**:
87
+ """
88
+ )
89
  print("✅ Models and prompt loaded successfully!")
90
  except Exception as e:
91
+ print("❌ Lifespan error:", str(e))
92
  raise e
93
 
94
  yield
95
  print("🧹 Cleaning up.")
96
  ml_models.clear()
 
97
  # --- 2. FastAPI App Instance ---
98
+ # We pass the lifespan function to the FastAPI constructor
99
  app = FastAPI(title="HackRX RAG Server", lifespan=lifespan)
100
 
 
101
  # --- 3. API Key Verification ---
102
  TEAM_API_KEY = os.getenv("TEAM_API_KEY")
103
 
 
105
  if not authorization.startswith("Bearer "):
106
  raise HTTPException(status_code=401, detail="Invalid Authorization header format")
107
  token = authorization.split("Bearer ")[1]
108
+ # print(token)
109
+ # print(TEAM_API_KEY)
110
  if token != TEAM_API_KEY:
111
  raise HTTPException(status_code=403, detail="Invalid or missing API key")
112
 
113
+
114
  # --- 4. Parsing Helper ---
115
  def parse_llm_response(content: str) -> str:
116
  try:
117
+ # Remove code fences and clean up
118
  content_cleaned = re.sub(r"^```json|```$", "", content.strip(), flags=re.IGNORECASE).strip()
119
  data = json.loads(content_cleaned)
120
+
121
+ if isinstance(data, dict):
122
+ if "decision" in data:
123
+ decision = data.get("decision", "N/A").upper()
124
+ amount = data.get("amount", "Not specified")
125
+ justification = data.get("justification", "No justification provided.")
126
+ return f"Decision: {decision}\nAmount: {amount}\nJustification: {justification}"
127
+
128
+ elif "response" in data:
129
+ return data["response"]
130
+
131
+ return "The response was parsed but didn't match expected structure."
132
+
133
  except json.JSONDecodeError:
134
+ return f"Unstructured response:\n{content.strip()}"
135
+
136
  except Exception as e:
137
+ return f"An error occurred while processing the response: {str(e)}"
138
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
 
140
+ # --- 5. Main API Endpoint ---
141
  @app.post("/api/v1/hackrx/run", response_model=RunResponse, dependencies=[Depends(verify_api_key)])
142
  async def run_hackrx(req: RunRequest):
143
+ chunks = load_and_chunk(str(req.documents))
 
 
 
 
 
 
 
 
 
 
 
 
144
  if not chunks:
145
+ return JSONResponse({"error": "No documents could be processed."}, status_code=400)
146
+
147
+
148
+ ####code for parallel####################################################################################
149
+
150
+ vectorstore = await FAISS.afrom_documents(
151
+ documents=chunks,
152
+ embedding=ml_models["embedder"]
153
+ )
154
+
155
+ # dense_retriever = vectorstore.as_retriever(search_type="mmr",search_kwargs={"k": 8})
156
+ dense_retriever = vectorstore.as_retriever(search_type="mmr",search_kwargs={"k": 8 ,"lambda_mult": 0.6})
157
+
158
 
159
+ # Create retrievers using the pre-loaded models from our ml_models dictionary
 
160
  keyword_retriever = BM25Retriever.from_documents(chunks)
161
+ keyword_retriever.k = 5
162
+ # dense_retriever = Chroma.from_documents(documents=chunks, embedding=ml_models["embedder"]).as_retriever()
163
+ ensemble_retriever = EnsembleRetriever(retrievers=[keyword_retriever, dense_retriever], weights=[0.35, 0.65])
164
+ ### to make it faster we are now using our built reranker thats why commenting the code below
165
+ compression_retriever = ContextualCompressionRetriever(
166
+ base_retriever=ensemble_retriever, base_compressor=ml_models["reranker_compressor"]
167
+ )
168
 
 
 
 
 
 
 
 
 
 
 
 
169
 
170
+ # Define the RAG chain using pre-loaded components
171
+ hybrid_rag_chain = (
172
+ {"context": itemgetter("full_query") | compression_retriever, "full_query": itemgetter("full_query")}
173
+ | ml_models["prompt_template"]
174
+ | ml_models["llm"]
175
+ )
176
+
177
+ ######## OUR SELF RERANKER ######################################################################
178
+ #Embed all questions at once
179
+ # question_embeddings = ml_models["embedder"].embed_documents(req.questions)
180
+
181
+ # For each question, retrieve and rerank with cosine
182
+ # retrieved_chunks_all = []
183
+ # for i, question in enumerate(req.questions):
184
+ # docs = ensemble_retriever.get_relevant_documents(question)
185
+ # doc_texts = [doc.page_content for doc in docs]
186
+
187
+ # doc_embeddings = ml_models["embedder"].embed_documents(doc_texts)
188
+ # sims = cosine_similarity([question_embeddings[i]], doc_embeddings)[0]
189
+
190
+ # top_k = 5
191
+ # top_indices = np.argsort(sims)[-top_k:][::-1]
192
+ # top_chunks = [doc_texts[j] for j in top_indices]
193
+
194
+ # # Join for context
195
+ # joined_context = "\n\n".join(top_chunks)
196
+ # retrieved_chunks_all.append(joined_context)
197
+ # def mmr_select(query_embedding, doc_embeddings, k=6, lambda_mult=0.6):
198
+ # selected = []
199
+ # candidates = list(range(len(doc_embeddings)))
200
+ # doc_embeddings = np.array(doc_embeddings)
201
 
202
+ # # Convert query_embedding to 2D
203
+ # query_embedding = np.array(query_embedding).reshape(1, -1)
204
 
205
+ # # Compute similarity between query and all documents
206
+ # query_doc_sims = cosine_similarity(query_embedding, doc_embeddings)[0]
207
+
208
+ # for _ in range(k):
209
+ # mmr_score = []
210
+ # for idx in candidates:
211
+ # if not selected:
212
+ # diversity = 0
213
+ # else:
214
+ # selected_embeddings = doc_embeddings[selected]
215
+ # diversity = max(cosine_similarity(
216
+ # doc_embeddings[idx].reshape(1, -1),
217
+ # selected_embeddings
218
+ # )[0])
219
+ # score = lambda_mult * query_doc_sims[idx] - (1 - lambda_mult) * diversity
220
+ # mmr_score.append(score)
221
+ # selected_idx = candidates[np.argmax(mmr_score)]
222
+ # selected.append(selected_idx)
223
+ # candidates.remove(selected_idx)
224
+
225
+ # return selected
226
+ # async def async_retrieve_and_rerank(question: str, q_idx: int):
227
+ # docs = await ensemble_retriever.ainvoke(question)
228
+ # doc_texts = [doc.page_content for doc in docs]
229
+ # doc_embeddings = ml_models["embedder"].embed_documents(doc_texts)
230
+ # # sims = cosine_similarity([question_embeddings[q_idx]], doc_embeddings)[0]
231
+ # query_embedding = question_embeddings[q_idx]
232
+ # selected_indices = mmr_select(
233
+ # query_embedding=query_embedding,
234
+ # doc_embeddings=doc_embeddings,
235
+ # k=6,
236
+ # lambda_mult=0.6,
237
+ # )
238
+ # # top_indices = np.argsort(sims)[-top_k:][::-1]
239
+ # top_chunks = [doc_texts[j] for j in selected_indices]
240
+ # return "\n\n".join(top_chunks)
241
+ # # Retrieve and rerank all in parallel
242
+ # retrieved_chunks_all = await asyncio.gather(
243
+ # *[async_retrieve_and_rerank(q, i) for i, q in enumerate(req.questions)]
244
+ # )
245
+
246
+ ####################################################################################################################
247
+
248
+ # tasks = []
249
+ # for i in range(len(req.questions)):
250
+ # prompt_input = {
251
+ # "full_query": req.questions[i],
252
+ # "context": retrieved_chunks_all[i]
253
+ # }
254
+ # tasks.append(ml_models["llm"].ainvoke(ml_models["prompt_template"].format_prompt(**prompt_input)))
255
+ # # tasks = [hybrid_rag_chain.ainvoke({"full_query": q}) for q in req.questions]
256
+ # results = await asyncio.gather(*tasks)
257
+ # answers = []
258
+
259
+ # for msg in results:
260
+ # # Safely access the content field
261
+ # if hasattr(msg, "content"):
262
+ # answers.append(msg.content.strip())
263
+ # # Extract the content from each result and parse it
264
+ tasks = [hybrid_rag_chain.ainvoke({"full_query": q}) for q in req.questions]
265
+ results = await asyncio.gather(*tasks)
266
+ # answers = [parse_llm_response(result.content) for result in results]
267
+ answers = []
268
+
269
+ for msg in results:
270
+ # Safely access the content field
271
+ if hasattr(msg, "content"):
272
+ response.append(msg.content.strip())
273
+
274
  return JSONResponse({"answers": answers}, status_code=200)
275
 
276
  @app.get("/", include_in_schema=False)
277
  def root():
278
+ return {"message": "API is running. Go to /docs for documentation."}
279
+
280
+
281
+ # def dummy_gradio(): return "✅ API running!"
282
+ # gr.Interface(fn=dummy_gradio, inputs=[], outputs="text").launch(server_name="0.0.0.0", server_port=7860)