singhankur01 commited on
Commit
55f4347
·
verified ·
1 Parent(s): d300309

Update app.py

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