singhankur01 commited on
Commit
cdbf2f9
·
verified ·
1 Parent(s): 7d2bf8b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +68 -201
app.py CHANGED
@@ -1,4 +1,3 @@
1
-
2
  import os
3
  import json
4
  import re
@@ -7,40 +6,32 @@ 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:
@@ -49,63 +40,47 @@ async def lifespan(app: FastAPI):
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
-
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
- You are an expert decision maker Assistant in the domain such as insurance, legal compliance, human resources, and contract management.
73
- Do not add any irrelevent information, also do not use bullet points, make it in a perfect sentence answer.
74
- The customer has submitted a query. If the query has details about age , gender ,procedure ,location , policy duration, then parse it and understand the query properly
75
- If not, the raw question is provided instead:
76
- - Query: {full_query}
77
-
78
- We retrieved the following policy clauses and rules relevant to this case:
79
  {context}
80
 
81
- Remember the below three points while answering or responsing the query :
82
- 1:Do not mention document id or its page number just ,mention clauses if applicable .
83
- 2:Do not make such statement in the response that you need more document or information to answer the query, just answer from retrieved data.
84
- ### Task:
85
- If the details about age , gender , procedure , location , policy duration are available, do all of the following:
86
- 1. Decide whether the procedure is covered.
87
- 2. Estimate the claimable amount.
88
- 3. Justify with the relevant clause.
89
- and answer the query precisely as insurance agent.
90
-
91
- Otherwise, answer the question concisely , relevently and clearly using the retrieved context.
92
-
93
- ### Output format:
94
- If query involes age , gender , procedure , location , policy duration answer like below:
95
- {{
96
- "decision": "approved / rejected",
97
- "amount": "INR amount or null",
98
- "justification": "Refer to specific clause"
99
- Make it concise ,perfect, relevent and clear .
100
- }}
101
- Else:
102
- {{
103
- "response": "Concise natural language answer"
104
- Make it concise ,perfect, relevent and clear .
105
- }}
106
 
107
  """
108
- )
 
109
  print("✅ Models and prompt loaded successfully!")
110
  except Exception as e:
111
  print("❌ Lifespan error:", str(e))
@@ -114,27 +89,20 @@ Else:
114
  yield
115
  print("🧹 Cleaning up.")
116
  ml_models.clear()
117
- # --- 2. FastAPI App Instance ---
118
- # We pass the lifespan function to the FastAPI constructor
119
  app = FastAPI(title="HackRX RAG Server", lifespan=lifespan)
120
 
121
- # --- 3. API Key Verification ---
122
  TEAM_API_KEY = os.getenv("TEAM_API_KEY")
123
 
124
  def verify_api_key(authorization: str = Header(...)):
125
  if not authorization.startswith("Bearer "):
126
  raise HTTPException(status_code=401, detail="Invalid Authorization header format")
127
  token = authorization.split("Bearer ")[1]
128
- # print(token)
129
- # print(TEAM_API_KEY)
130
  if token != TEAM_API_KEY:
131
  raise HTTPException(status_code=403, detail="Invalid or missing API key")
132
 
133
-
134
- # --- 4. Parsing Helper ---
135
  def parse_llm_response(content: str) -> str:
136
  try:
137
- # Remove code fences and clean up
138
  content_cleaned = re.sub(r"^```json|```$", "", content.strip(), flags=re.IGNORECASE).strip()
139
  data = json.loads(content_cleaned)
140
 
@@ -144,159 +112,58 @@ def parse_llm_response(content: str) -> str:
144
  amount = data.get("amount", "Not specified")
145
  justification = data.get("justification", "No justification provided.")
146
  return f"Decision: {decision}\nAmount: {amount}\nJustification: {justification}"
147
-
148
  elif "response" in data:
149
  return data["response"]
150
 
151
  return "The response was parsed but didn't match expected structure."
152
-
153
  except json.JSONDecodeError:
154
  return f"Unstructured response:\n{content.strip()}"
155
-
156
  except Exception as e:
157
  return f"An error occurred while processing the response: {str(e)}"
158
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
 
160
- # --- 5. Main API Endpoint ---
161
  @app.post("/api/v1/hackrx/run", response_model=RunResponse, dependencies=[Depends(verify_api_key)])
162
  async def run_hackrx(req: RunRequest):
163
  chunks = load_and_chunk(str(req.documents))
164
  if not chunks:
165
  return JSONResponse({"error": "No documents could be processed."}, status_code=400)
166
 
167
-
168
- ####code for parallel####################################################################################
169
-
170
- vectorstore = await FAISS.afrom_documents(
171
- documents=chunks,
172
- embedding=ml_models["embedder"]
173
- )
174
-
175
- # dense_retriever = vectorstore.as_retriever(search_type="mmr",search_kwargs={"k": 8})
176
- dense_retriever = vectorstore.as_retriever(search_type="mmr",search_kwargs={"k": 8 ,"lambda_mult": 0.5})
177
-
178
-
179
- # Create retrievers using the pre-loaded models from our ml_models dictionary
180
  keyword_retriever = BM25Retriever.from_documents(chunks)
181
  keyword_retriever.k = 5
182
- # dense_retriever = Chroma.from_documents(documents=chunks, embedding=ml_models["embedder"]).as_retriever()
183
  ensemble_retriever = EnsembleRetriever(retrievers=[keyword_retriever, dense_retriever], weights=[0.35, 0.65])
184
- ### to make it faster we are now using our built reranker thats why commenting the code below
185
- compression_retriever = ContextualCompressionRetriever(
186
- base_retriever=ensemble_retriever, base_compressor=ml_models["reranker_compressor"]
187
- )
188
-
189
-
190
- # Define the RAG chain using pre-loaded components
191
- hybrid_rag_chain = (
192
- {"context": itemgetter("full_query") | compression_retriever, "full_query": itemgetter("full_query")}
193
- | ml_models["prompt_template"]
194
- | ml_models["llm"]
195
- )
196
-
197
- ######## OUR SELF RERANKER ######################################################################
198
- #Embed all questions at once
199
- # question_embeddings = ml_models["embedder"].embed_documents(req.questions)
200
-
201
- # For each question, retrieve and rerank with cosine
202
- # retrieved_chunks_all = []
203
- # for i, question in enumerate(req.questions):
204
- # docs = ensemble_retriever.get_relevant_documents(question)
205
- # doc_texts = [doc.page_content for doc in docs]
206
-
207
- # doc_embeddings = ml_models["embedder"].embed_documents(doc_texts)
208
- # sims = cosine_similarity([question_embeddings[i]], doc_embeddings)[0]
209
-
210
- # top_k = 5
211
- # top_indices = np.argsort(sims)[-top_k:][::-1]
212
- # top_chunks = [doc_texts[j] for j in top_indices]
213
-
214
- # # Join for context
215
- # joined_context = "\n\n".join(top_chunks)
216
- # retrieved_chunks_all.append(joined_context)
217
- # def mmr_select(query_embedding, doc_embeddings, k=6, lambda_mult=0.6):
218
- # selected = []
219
- # candidates = list(range(len(doc_embeddings)))
220
- # doc_embeddings = np.array(doc_embeddings)
221
-
222
- # # Convert query_embedding to 2D
223
- # query_embedding = np.array(query_embedding).reshape(1, -1)
224
-
225
- # # Compute similarity between query and all documents
226
- # query_doc_sims = cosine_similarity(query_embedding, doc_embeddings)[0]
227
-
228
- # for _ in range(k):
229
- # mmr_score = []
230
- # for idx in candidates:
231
- # if not selected:
232
- # diversity = 0
233
- # else:
234
- # selected_embeddings = doc_embeddings[selected]
235
- # diversity = max(cosine_similarity(
236
- # doc_embeddings[idx].reshape(1, -1),
237
- # selected_embeddings
238
- # )[0])
239
- # score = lambda_mult * query_doc_sims[idx] - (1 - lambda_mult) * diversity
240
- # mmr_score.append(score)
241
- # selected_idx = candidates[np.argmax(mmr_score)]
242
- # selected.append(selected_idx)
243
- # candidates.remove(selected_idx)
244
-
245
- # return selected
246
- # async def async_retrieve_and_rerank(question: str, q_idx: int):
247
- # docs = await ensemble_retriever.ainvoke(question)
248
- # doc_texts = [doc.page_content for doc in docs]
249
- # doc_embeddings = ml_models["embedder"].embed_documents(doc_texts)
250
- # # sims = cosine_similarity([question_embeddings[q_idx]], doc_embeddings)[0]
251
- # query_embedding = question_embeddings[q_idx]
252
- # selected_indices = mmr_select(
253
- # query_embedding=query_embedding,
254
- # doc_embeddings=doc_embeddings,
255
- # k=6,
256
- # lambda_mult=0.6,
257
- # )
258
- # # top_indices = np.argsort(sims)[-top_k:][::-1]
259
- # top_chunks = [doc_texts[j] for j in selected_indices]
260
- # return "\n\n".join(top_chunks)
261
- # # Retrieve and rerank all in parallel
262
- # retrieved_chunks_all = await asyncio.gather(
263
- # *[async_retrieve_and_rerank(q, i) for i, q in enumerate(req.questions)]
264
- # )
265
-
266
- ####################################################################################################################
267
-
268
- # tasks = []
269
- # for i in range(len(req.questions)):
270
- # prompt_input = {
271
- # "full_query": req.questions[i],
272
- # "context": retrieved_chunks_all[i]
273
- # }
274
- # tasks.append(ml_models["llm"].ainvoke(ml_models["prompt_template"].format_prompt(**prompt_input)))
275
- # # tasks = [hybrid_rag_chain.ainvoke({"full_query": q}) for q in req.questions]
276
- # results = await asyncio.gather(*tasks)
277
- # answers = []
278
-
279
- # for msg in results:
280
- # # Safely access the content field
281
- # if hasattr(msg, "content"):
282
- # answers.append(msg.content.strip())
283
- # # Extract the content from each result and parse it
284
- tasks = [hybrid_rag_chain.ainvoke({"full_query": q}) for q in req.questions]
285
- results = await asyncio.gather(*tasks)
286
- answers = [parse_llm_response(result.content) for result in results]
287
- # answers = []
288
 
289
- # for msg in results:
290
- # # Safely access the content field
291
- # if hasattr(msg, "content"):
292
- # answers.append(msg.content.strip())
 
 
 
 
 
 
293
 
 
 
294
  return JSONResponse({"answers": answers}, status_code=200)
295
 
296
  @app.get("/", include_in_schema=False)
297
  def root():
298
  return {"message": "API is running. Go to /docs for documentation."}
299
-
300
-
301
- # def dummy_gradio(): return "✅ API running!"
302
- # gr.Interface(fn=dummy_gradio, inputs=[], outputs="text").launch(server_name="0.0.0.0", server_port=7860)
 
 
1
  import os
2
  import json
3
  import re
 
6
  from dotenv import load_dotenv
7
  from operator import itemgetter
8
 
 
9
  from fastapi import FastAPI, Depends, HTTPException, Header
10
  from fastapi.responses import JSONResponse
11
 
12
  from utils.DocsLoader import load_and_chunk
13
  from utils.Schemas import RunRequest, RunResponse
14
+
15
  from langchain_community.vectorstores import FAISS
16
  from langchain.schema import Document
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
  import numpy as np
23
 
 
24
  from langchain.retrievers import ContextualCompressionRetriever
25
+ from langchain.retrievers.document_compressors import CrossEncoderReranker
26
  from langchain_community.cross_encoders import HuggingFaceCrossEncoder
27
+ from langchain.prompts import ChatPromptTemplate
28
 
 
 
29
  load_dotenv()
30
 
 
 
31
  ml_models = {}
32
 
33
  @asynccontextmanager
34
  async def lifespan(app: FastAPI):
 
35
  print("🚀 Initializing models and prompt template...")
36
 
37
  try:
 
40
 
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={"batch_size": 64}
47
+ )
48
+
 
 
 
 
49
  cross_encoder_model = HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-base")
 
50
  ml_models["reranker_compressor"] = CrossEncoderReranker(model=cross_encoder_model, top_n=5)
51
  ml_models["llm"] = ChatGoogleGenerativeAI(
52
+ model="gemini-1.5-pro",
53
+ api_key=GOOGLE_API_KEY,
54
+ temperature=0.1,
55
+ max_output_tokens=300
56
+ )
57
+
58
+ ml_models["prompt_template"] = ChatPromptTemplate.from_template("""
59
+ **Role**: You are an expert decision maker Assistant in the domain such as insurance, legal compliance, human resources, and contract management using ONLY the provided context.
60
+ Do not use your own knowledge.
61
+ **Context**:
 
 
 
62
  {context}
63
 
64
+ **Query**: {full_query}
65
+
66
+ **Response**:
67
+
68
+ Example how to answer for thr query:
69
+ 1. query:What is the grace period for premium payment under the National Parivar Mediclaim Plus Policy?
70
+ response: A grace period of thirty days is provided for premium payment after the due date to renew or continue the policy without losing continuity benefits.
71
+
72
+ 2. query: What is the waiting period for pre-existing diseases (PED) to be covered?
73
+ response: There is a waiting period of thirty-six (36) months of continuous coverage from the first policy inception for pre-existing diseases and their direct complications to be covered.
74
+
75
+ 3. query: Are the medical expenses for an organ donor covered under this policy?
76
+ response : Yes, the policy indemnifies the medical expenses for the organ donor's hospitalization for the purpose of harvesting the organ, provided the organ is for an insured person and the donation complies with the Transplantation of Human Organs Act, 1994.
77
+
78
+ 4. query: Does this policy cover maternity expenses, and what are the conditions?
79
+ response: Yes, the policy covers maternity expenses, including childbirth and lawful medical termination of pregnancy. To be eligible, the female insured person must have been continuously covered for at least 24 months. The benefit is limited to two deliveries or terminations during the policy period.
 
 
 
 
 
 
 
 
 
80
 
81
  """
82
+ )
83
+
84
  print("✅ Models and prompt loaded successfully!")
85
  except Exception as e:
86
  print("❌ Lifespan error:", str(e))
 
89
  yield
90
  print("🧹 Cleaning up.")
91
  ml_models.clear()
92
+
 
93
  app = FastAPI(title="HackRX RAG Server", lifespan=lifespan)
94
 
 
95
  TEAM_API_KEY = os.getenv("TEAM_API_KEY")
96
 
97
  def verify_api_key(authorization: str = Header(...)):
98
  if not authorization.startswith("Bearer "):
99
  raise HTTPException(status_code=401, detail="Invalid Authorization header format")
100
  token = authorization.split("Bearer ")[1]
 
 
101
  if token != TEAM_API_KEY:
102
  raise HTTPException(status_code=403, detail="Invalid or missing API key")
103
 
 
 
104
  def parse_llm_response(content: str) -> str:
105
  try:
 
106
  content_cleaned = re.sub(r"^```json|```$", "", content.strip(), flags=re.IGNORECASE).strip()
107
  data = json.loads(content_cleaned)
108
 
 
112
  amount = data.get("amount", "Not specified")
113
  justification = data.get("justification", "No justification provided.")
114
  return f"Decision: {decision}\nAmount: {amount}\nJustification: {justification}"
 
115
  elif "response" in data:
116
  return data["response"]
117
 
118
  return "The response was parsed but didn't match expected structure."
 
119
  except json.JSONDecodeError:
120
  return f"Unstructured response:\n{content.strip()}"
 
121
  except Exception as e:
122
  return f"An error occurred while processing the response: {str(e)}"
123
 
124
+ # --- Post-retrieval Filters ---
125
+ def structure_filter(docs, min_length=30):
126
+ return [doc for doc in docs if len(doc.page_content.strip()) >= min_length and not doc.page_content.isspace()]
127
+
128
+ def semantic_filter(query, docs, embedder, threshold=0.45):
129
+ query_emb = embedder.embed_query(query)
130
+ doc_texts = [doc.page_content for doc in docs]
131
+ doc_embs = embedder.embed_documents(doc_texts)
132
+ sims = cosine_similarity([query_emb], doc_embs)[0]
133
+ return [doc for doc, sim in zip(docs, sims) if sim >= threshold]
134
+
135
+ def universal_filter(query, docs, embedder):
136
+ docs = structure_filter(docs)
137
+ docs = semantic_filter(query, docs, embedder)
138
+ return docs if docs else docs # fallback to raw if filtering removes all
139
 
 
140
  @app.post("/api/v1/hackrx/run", response_model=RunResponse, dependencies=[Depends(verify_api_key)])
141
  async def run_hackrx(req: RunRequest):
142
  chunks = load_and_chunk(str(req.documents))
143
  if not chunks:
144
  return JSONResponse({"error": "No documents could be processed."}, status_code=400)
145
 
146
+ vectorstore = await FAISS.afrom_documents(documents=chunks, embedding=ml_models["embedder"])
147
+ dense_retriever = vectorstore.as_retriever(search_type="mmr", search_kwargs={"k": 8, "lambda_mult": 0.5})
 
 
 
 
 
 
 
 
 
 
 
148
  keyword_retriever = BM25Retriever.from_documents(chunks)
149
  keyword_retriever.k = 5
 
150
  ensemble_retriever = EnsembleRetriever(retrievers=[keyword_retriever, dense_retriever], weights=[0.35, 0.65])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
 
152
+ tasks = []
153
+ for query in req.questions:
154
+ raw_docs = await ensemble_retriever.ainvoke(query)
155
+ filtered_docs = universal_filter(query, raw_docs, ml_models["embedder"])
156
+ if not filtered_docs:
157
+ filtered_docs = raw_docs # fallback
158
+ reranked_docs = ml_models["reranker_compressor"].compress_documents(filtered_docs, query=query)
159
+ context = "\n\n".join([doc.page_content for doc in reranked_docs])
160
+ prompt_input = {"full_query": query, "context": context}
161
+ tasks.append(ml_models["llm"].ainvoke(ml_models["prompt_template"].format_prompt(**prompt_input)))
162
 
163
+ results = await asyncio.gather(*tasks)
164
+ answers = [parse_llm_response(result.content) for result in results]
165
  return JSONResponse({"answers": answers}, status_code=200)
166
 
167
  @app.get("/", include_in_schema=False)
168
  def root():
169
  return {"message": "API is running. Go to /docs for documentation."}