singhankur01 commited on
Commit
4967cd0
·
verified ·
1 Parent(s): 795cc53

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +168 -152
app.py CHANGED
@@ -1,62 +1,51 @@
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 insurance assistant. Your task is to answer the user's question as concisely as possible using ONLY the provided context.
62
 
@@ -70,8 +59,8 @@ async def lifespan(app: FastAPI):
70
 
71
  Concise Answer:
72
  """
73
- )
74
- print("✅ Models and prompt loaded successfully!")
75
  except Exception as e:
76
  print("❌ Lifespan error:", str(e))
77
  raise e
@@ -79,156 +68,190 @@ async def lifespan(app: FastAPI):
79
  yield
80
  print("🧹 Cleaning up.")
81
  ml_models.clear()
82
- # --- 2. FastAPI App Instance ---
83
- # We pass the lifespan function to the FastAPI constructor
84
  app = FastAPI(title="HackRX RAG Server", lifespan=lifespan)
85
 
86
- # --- 3. API Key Verification ---
87
  TEAM_API_KEY = os.getenv("TEAM_API_KEY")
88
 
89
  def verify_api_key(authorization: str = Header(...)):
90
  if not authorization.startswith("Bearer "):
91
  raise HTTPException(status_code=401, detail="Invalid Authorization header format")
92
  token = authorization.split("Bearer ")[1]
93
- # print(token)
94
- # print(TEAM_API_KEY)
95
  if token != TEAM_API_KEY:
96
  raise HTTPException(status_code=403, detail="Invalid or missing API key")
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
 
99
- # --- 4. Parsing Helper ---
100
- def parse_llm_response(content: str) -> str:
101
  try:
102
- # Remove code fences and clean up
103
- content_cleaned = re.sub(r"^```json|```$", "", content.strip(), flags=re.IGNORECASE).strip()
104
- data = json.loads(content_cleaned)
105
-
106
- if isinstance(data, dict):
107
- if "decision" in data:
108
- decision = data.get("decision", "N/A").upper()
109
- amount = data.get("amount", "Not specified")
110
- justification = data.get("justification", "No justification provided.")
111
- return f"Decision: {decision}\nAmount: {amount}\nJustification: {justification}"
112
-
113
- elif "response" in data:
114
- return data["response"]
115
-
116
- return "The response was parsed but didn't match expected structure."
117
-
118
- except json.JSONDecodeError:
119
- return f"Unstructured response:\n{content.strip()}"
120
-
121
  except Exception as e:
122
- return f"An error occurred while processing the response: {str(e)}"
 
123
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
 
125
- # --- 5. Main API Endpoint ---
126
  @app.post("/api/v1/hackrx/run", response_model=RunResponse, dependencies=[Depends(verify_api_key)])
127
  async def run_hackrx(req: RunRequest):
128
  chunks = load_and_chunk(str(req.documents))
129
  if not chunks:
130
  return JSONResponse({"error": "No documents could be processed."}, status_code=400)
131
 
132
-
133
- ####code for parallel####################################################################################
134
-
135
- vectorstore = await FAISS.afrom_documents(
136
- documents=chunks,
137
- embedding=ml_models["embedder"]
138
- )
139
-
140
- dense_retriever = vectorstore.as_retriever(search_type="mmr",search_kwargs={"k": 8})
141
-
142
-
143
- # Create retrievers using the pre-loaded models from our ml_models dictionary
144
- keyword_retriever = BM25Retriever.from_documents(chunks)
145
- keyword_retriever.k = 5
146
- # dense_retriever = Chroma.from_documents(documents=chunks, embedding=ml_models["embedder"]).as_retriever()
147
- ensemble_retriever = EnsembleRetriever(retrievers=[keyword_retriever, dense_retriever], weights=[0.4, 0.6])
148
- ### to make it faster we are now using our built reranker thats why commenting the code below
149
- # compression_retriever = ContextualCompressionRetriever(
150
- # base_retriever=ensemble_retriever, base_compressor=ml_models["reranker_compressor"]
151
- # )
152
 
 
 
153
 
154
- # Define the RAG chain using pre-loaded components
155
- # hybrid_rag_chain = (
156
- # {"context": itemgetter("full_query") | compression_retriever, "full_query": itemgetter("full_query")}
157
- # | ml_models["prompt_template"]
158
- # | ml_models["llm"]
159
- # )
160
-
161
- ######## OUR SELF RERANKER ######################################################################
162
- #Embed all questions at once
163
- question_embeddings = ml_models["embedder"].embed_documents(req.questions)
164
-
165
- # For each question, retrieve and rerank with cosine
166
- # retrieved_chunks_all = []
167
- # for i, question in enumerate(req.questions):
168
- # docs = ensemble_retriever.get_relevant_documents(question)
169
- # doc_texts = [doc.page_content for doc in docs]
170
-
171
- # doc_embeddings = ml_models["embedder"].embed_documents(doc_texts)
172
- # sims = cosine_similarity([question_embeddings[i]], doc_embeddings)[0]
173
-
174
- # top_k = 5
175
- # top_indices = np.argsort(sims)[-top_k:][::-1]
176
- # top_chunks = [doc_texts[j] for j in top_indices]
177
-
178
- # # Join for context
179
- # joined_context = "\n\n".join(top_chunks)
180
- # retrieved_chunks_all.append(joined_context)
181
- def mmr_select(query_embedding, doc_embeddings, k=6, lambda_mult=0.6):
182
- selected = []
183
- candidates = list(range(len(doc_embeddings)))
184
- doc_embeddings = np.array(doc_embeddings)
185
 
186
- # Convert query_embedding to 2D
187
- query_embedding = np.array(query_embedding).reshape(1, -1)
188
 
189
- # Compute similarity between query and all documents
190
- query_doc_sims = cosine_similarity(query_embedding, doc_embeddings)[0]
 
 
 
 
 
 
 
 
 
 
191
 
192
- for _ in range(k):
193
- mmr_score = []
194
- for idx in candidates:
195
- if not selected:
196
- diversity = 0
197
- else:
198
- selected_embeddings = doc_embeddings[selected]
199
- diversity = max(cosine_similarity(
200
- doc_embeddings[idx].reshape(1, -1),
201
- selected_embeddings
202
- )[0])
203
- score = lambda_mult * query_doc_sims[idx] - (1 - lambda_mult) * diversity
204
- mmr_score.append(score)
205
- selected_idx = candidates[np.argmax(mmr_score)]
206
- selected.append(selected_idx)
207
- candidates.remove(selected_idx)
208
 
209
- return selected
 
 
 
 
 
210
  async def async_retrieve_and_rerank(question: str, q_idx: int):
211
- docs = await ensemble_retriever.ainvoke(question)
212
- doc_texts = [doc.page_content for doc in docs]
213
- doc_embeddings = ml_models["embedder"].embed_documents(doc_texts)
214
- # sims = cosine_similarity([question_embeddings[q_idx]], doc_embeddings)[0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  query_embedding = question_embeddings[q_idx]
216
  selected_indices = mmr_select(
217
- query_embedding=query_embedding,
218
- doc_embeddings=doc_embeddings,
219
- k=6,
220
- lambda_mult=0.6,
221
- )
222
- # top_indices = np.argsort(sims)[-top_k:][::-1]
223
- top_chunks = [doc_texts[j] for j in selected_indices]
224
  return "\n\n".join(top_chunks)
225
- # Retrieve and rerank all in parallel
 
226
  retrieved_chunks_all = await asyncio.gather(
227
  *[async_retrieve_and_rerank(q, i) for i, q in enumerate(req.questions)]
228
- )
229
-
230
- ####################################################################################################################
231
 
 
232
  tasks = []
233
  for i in range(len(req.questions)):
234
  prompt_input = {
@@ -236,23 +259,16 @@ async def run_hackrx(req: RunRequest):
236
  "context": retrieved_chunks_all[i]
237
  }
238
  tasks.append(ml_models["llm"].ainvoke(ml_models["prompt_template"].format_prompt(**prompt_input)))
239
- # tasks = [hybrid_rag_chain.ainvoke({"full_query": q}) for q in req.questions]
240
  results = await asyncio.gather(*tasks)
241
  answers = []
242
 
243
  for msg in results:
244
- # Safely access the content field
245
  if hasattr(msg, "content"):
246
  answers.append(msg.content.strip())
247
- # Extract the content from each result and parse it
248
- # answers = [parse_llm_response(result.content) for result in results]
249
 
250
  return JSONResponse({"answers": answers}, status_code=200)
251
 
252
  @app.get("/", include_in_schema=False)
253
  def root():
254
- return {"message": "API is running. Go to /docs for documentation."}
255
-
256
-
257
- # def dummy_gradio(): return "✅ API running!"
258
- # 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
  from contextlib import asynccontextmanager
6
  from dotenv import load_dotenv
 
7
 
 
8
  from fastapi import FastAPI, Depends, HTTPException, Header
9
  from fastapi.responses import JSONResponse
10
 
11
  from utils.DocsLoader import load_and_chunk
12
  from utils.Schemas import RunRequest, RunResponse
 
 
13
  from langchain.schema import Document
14
  from langchain_google_genai import ChatGoogleGenerativeAI
 
 
 
 
15
  from sklearn.metrics.pairwise import cosine_similarity
16
  import numpy as np
 
 
 
 
 
17
  from langchain.prompts import PromptTemplate
18
 
19
+ # Pinecone imports
20
+ from pinecone import Pinecone
21
 
22
  # Load environment variables
23
  load_dotenv()
24
 
 
25
  # This dictionary will hold our loaded models
26
  ml_models = {}
27
 
28
  @asynccontextmanager
29
  async def lifespan(app: FastAPI):
30
+ print("🚀 Initializing Pinecone and models...")
 
31
 
32
  try:
33
  GOOGLE_API_KEY = os.getenv("gemini_api_key")
34
+ PINECONE_API_KEY = os.getenv("PINECONE_API_KEY")
35
+
36
  print("🔑 gemini_api_key:", "FOUND" if GOOGLE_API_KEY else "NOT FOUND")
37
+ print("🔑 pinecone_api_key:", "FOUND" if PINECONE_API_KEY else "NOT FOUND")
38
 
39
+ if not GOOGLE_API_KEY or not PINECONE_API_KEY:
40
+ raise RuntimeError("CRITICAL: Missing API keys in environment!")
41
+
42
+ # Initialize Pinecone client
43
+ ml_models["pc"] = Pinecone(api_key=PINECONE_API_KEY)
44
+
45
+ # Initialize LLM
 
 
46
  ml_models["llm"] = ChatGoogleGenerativeAI(model="gemini-2.0-flash", api_key=GOOGLE_API_KEY)
47
+
48
+ # Prompt template
49
  ml_models["prompt_template"] = PromptTemplate.from_template(
50
  """You are an expert insurance assistant. Your task is to answer the user's question as concisely as possible using ONLY the provided context.
51
 
 
59
 
60
  Concise Answer:
61
  """
62
+ )
63
+ print("✅ Pinecone and models loaded successfully!")
64
  except Exception as e:
65
  print("❌ Lifespan error:", str(e))
66
  raise e
 
68
  yield
69
  print("🧹 Cleaning up.")
70
  ml_models.clear()
71
+
 
72
  app = FastAPI(title="HackRX RAG Server", lifespan=lifespan)
73
 
74
+ # API Key Verification
75
  TEAM_API_KEY = os.getenv("TEAM_API_KEY")
76
 
77
  def verify_api_key(authorization: str = Header(...)):
78
  if not authorization.startswith("Bearer "):
79
  raise HTTPException(status_code=401, detail="Invalid Authorization header format")
80
  token = authorization.split("Bearer ")[1]
 
 
81
  if token != TEAM_API_KEY:
82
  raise HTTPException(status_code=403, detail="Invalid or missing API key")
83
 
84
+ def create_serverless_index(pc, index_name):
85
+ """Create a serverless index for hybrid search"""
86
+ existing_indexes = [index.name for index in pc.list_indexes()]
87
+
88
+ if index_name not in existing_indexes:
89
+ pc.create_index(
90
+ name=index_name,
91
+ dimension=1024, # multilingual-e5-large dimension
92
+ metric="cosine"
93
+ )
94
+ print(f"✅ Created serverless index: {index_name}")
95
+ else:
96
+ print(f"📋 Index {index_name} already exists")
97
+
98
+ return pc.Index(index_name)
99
 
100
+ async def embed_with_pinecone_inference(pc, texts, model="multilingual-e5-large"):
101
+ """Use Pinecone's hosted multilingual-e5-large model for embeddings"""
102
  try:
103
+ # Use Pinecone Inference for embeddings
104
+ embeddings = pc.inference.embed(
105
+ model=model,
106
+ inputs=texts,
107
+ parameters={"input_type": "passage"}
108
+ )
109
+ return [embedding['values'] for embedding in embeddings]
 
 
 
 
 
 
 
 
 
 
 
 
110
  except Exception as e:
111
+ print(f" Embedding error: {e}")
112
+ raise
113
 
114
+ def generate_sparse_vectors(texts):
115
+ """Generate sparse vectors using simple term frequency"""
116
+ from collections import Counter
117
+ import re
118
+
119
+ sparse_vectors = []
120
+ for text in texts:
121
+ # Simple tokenization and term frequency
122
+ tokens = re.findall(r'\b\w+\b', text.lower())
123
+ token_counts = Counter(tokens)
124
+
125
+ # Create vocabulary mapping (simplified)
126
+ vocab = {token: i for i, token in enumerate(set(tokens))}
127
+
128
+ indices = []
129
+ values = []
130
+ for token, count in token_counts.items():
131
+ if token in vocab:
132
+ indices.append(vocab[token])
133
+ values.append(float(count))
134
+
135
+ sparse_vectors.append({
136
+ 'indices': indices,
137
+ 'values': values
138
+ })
139
+
140
+ return sparse_vectors
141
+
142
+ def mmr_select(query_embedding, doc_embeddings, k=6, lambda_mult=0.6):
143
+ selected = []
144
+ candidates = list(range(len(doc_embeddings)))
145
+ doc_embeddings = np.array(doc_embeddings)
146
+
147
+ query_embedding = np.array(query_embedding).reshape(1, -1)
148
+ query_doc_sims = cosine_similarity(query_embedding, doc_embeddings)[0]
149
+
150
+ for _ in range(k):
151
+ mmr_score = []
152
+ for idx in candidates:
153
+ if not selected:
154
+ diversity = 0
155
+ else:
156
+ selected_embeddings = doc_embeddings[selected]
157
+ diversity = max(cosine_similarity(
158
+ doc_embeddings[idx].reshape(1, -1),
159
+ selected_embeddings
160
+ )[0])
161
+ score = lambda_mult * query_doc_sims[idx] - (1 - lambda_mult) * diversity
162
+ mmr_score.append(score)
163
+ selected_idx = candidates[np.argmax(mmr_score)]
164
+ selected.append(selected_idx)
165
+ candidates.remove(selected_idx)
166
+
167
+ return selected
168
 
 
169
  @app.post("/api/v1/hackrx/run", response_model=RunResponse, dependencies=[Depends(verify_api_key)])
170
  async def run_hackrx(req: RunRequest):
171
  chunks = load_and_chunk(str(req.documents))
172
  if not chunks:
173
  return JSONResponse({"error": "No documents could be processed."}, status_code=400)
174
 
175
+ # Initialize Pinecone index
176
+ index_name = "insurance-hybrid-search"
177
+ pc = ml_models["pc"]
178
+ index = create_serverless_index(pc, index_name)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
 
180
+ # Prepare document texts
181
+ doc_texts = [doc.page_content for doc in chunks]
182
 
183
+ # Generate embeddings using Pinecone's multilingual-e5-large
184
+ dense_embeddings = await embed_with_pinecone_inference(pc, doc_texts)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
 
186
+ # Generate sparse vectors
187
+ sparse_embeddings = generate_sparse_vectors(doc_texts)
188
 
189
+ # Upsert to Pinecone with hybrid vectors
190
+ vectors_to_upsert = []
191
+ for i, (doc, dense_emb, sparse_emb) in enumerate(zip(chunks, dense_embeddings, sparse_embeddings)):
192
+ vectors_to_upsert.append({
193
+ 'id': f'doc_{i}',
194
+ 'values': dense_emb,
195
+ 'sparse_values': sparse_emb,
196
+ 'metadata': {
197
+ 'text': doc.page_content,
198
+ 'source': getattr(doc, 'metadata', {}).get('source', 'unknown')
199
+ }
200
+ })
201
 
202
+ # Batch upsert
203
+ batch_size = 100
204
+ for i in range(0, len(vectors_to_upsert), batch_size):
205
+ batch = vectors_to_upsert[i:i+batch_size]
206
+ index.upsert(vectors=batch)
 
 
 
 
 
 
 
 
 
 
 
207
 
208
+ print(f"✅ Upserted {len(vectors_to_upsert)} vectors to Pinecone")
209
+
210
+ # Generate question embeddings
211
+ question_embeddings = await embed_with_pinecone_inference(pc, req.questions)
212
+ question_sparse = generate_sparse_vectors(req.questions)
213
+
214
  async def async_retrieve_and_rerank(question: str, q_idx: int):
215
+ # Hybrid search with alpha=0.5 (balanced dense/sparse)
216
+ alpha = 0.5
217
+
218
+ # Scale embeddings for hybrid search
219
+ dense_query = [v * alpha for v in question_embeddings[q_idx]]
220
+ sparse_query = {
221
+ 'indices': question_sparse[q_idx]['indices'],
222
+ 'values': [v * (1 - alpha) for v in question_sparse[q_idx]['values']]
223
+ }
224
+
225
+ # Query Pinecone with hybrid search
226
+ results = index.query(
227
+ vector=dense_query,
228
+ sparse_vector=sparse_query,
229
+ top_k=8,
230
+ include_metadata=True
231
+ )
232
+
233
+ # Extract contexts and embeddings for MMR
234
+ contexts = [match['metadata']['text'] for match in results['matches']]
235
+ context_embeddings = [match['values'] for match in results['matches']]
236
+
237
+ # Apply MMR selection
238
  query_embedding = question_embeddings[q_idx]
239
  selected_indices = mmr_select(
240
+ query_embedding=query_embedding,
241
+ doc_embeddings=context_embeddings,
242
+ k=6,
243
+ lambda_mult=0.6,
244
+ )
245
+
246
+ top_chunks = [contexts[j] for j in selected_indices]
247
  return "\n\n".join(top_chunks)
248
+
249
+ # Retrieve and rerank all questions in parallel
250
  retrieved_chunks_all = await asyncio.gather(
251
  *[async_retrieve_and_rerank(q, i) for i, q in enumerate(req.questions)]
252
+ )
 
 
253
 
254
+ # Generate answers
255
  tasks = []
256
  for i in range(len(req.questions)):
257
  prompt_input = {
 
259
  "context": retrieved_chunks_all[i]
260
  }
261
  tasks.append(ml_models["llm"].ainvoke(ml_models["prompt_template"].format_prompt(**prompt_input)))
262
+
263
  results = await asyncio.gather(*tasks)
264
  answers = []
265
 
266
  for msg in results:
 
267
  if hasattr(msg, "content"):
268
  answers.append(msg.content.strip())
 
 
269
 
270
  return JSONResponse({"answers": answers}, status_code=200)
271
 
272
  @app.get("/", include_in_schema=False)
273
  def root():
274
+ return {"message": "API is running. Go to /docs for documentation."}