singhankur01 commited on
Commit
6824985
·
verified ·
1 Parent(s): 2afec42

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +153 -175
app.py CHANGED
@@ -1,51 +1,62 @@
 
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,8 +70,8 @@ async def lifespan(app: FastAPI):
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,196 +79,156 @@ async def lifespan(app: FastAPI):
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(pinecone_client, index_name):
85
- """Creates a Pinecone serverless index if it doesn't already exist."""
86
- existing_indexes = [index_info.name for index_info in pinecone_client.list_indexes().indexes]
87
- if index_name not in existing_indexes:
88
- print(f"🪄 Creating serverless index: {index_name}")
89
- pinecone_client.create_index(
90
- name=index_name,
91
- dimension=768, # or 384 depending on your embedding model
92
- metric='cosine',
93
- spec={
94
- 'serverless': {
95
- 'cloud': 'aws',
96
- 'region': 'us-east-1' # ✅ Required for Free Tier
97
- }
98
- }
99
- )
100
- print("✅ Index created.")
101
- else:
102
- print(f"⚠️ Index '{index_name}' already exists.")
103
- return pinecone_client.Index(index_name)
104
-
105
-
106
- async def embed_with_pinecone_inference(pc, texts, model="multilingual-e5-large"):
107
- """Use Pinecone's hosted inference model for embeddings."""
108
  try:
109
- inference_client = pc.inference
110
- embeddings = inference_client.embed(
111
- model=model,
112
- inputs=texts,
113
- parameters={"input_type": "passage"}
114
- )
115
- return [item["values"] for item in embeddings["results"]]
 
 
 
 
 
 
 
 
 
 
 
 
116
  except Exception as e:
117
- print(f" Pinecone Inference Error: {e}")
118
- raise
119
 
120
- def generate_sparse_vectors(texts):
121
- """Generate sparse vectors using simple term frequency"""
122
- from collections import Counter
123
- import re
124
-
125
- sparse_vectors = []
126
- for text in texts:
127
- # Simple tokenization and term frequency
128
- tokens = re.findall(r'\b\w+\b', text.lower())
129
- token_counts = Counter(tokens)
130
-
131
- # Create vocabulary mapping (simplified)
132
- vocab = {token: i for i, token in enumerate(set(tokens))}
133
-
134
- indices = []
135
- values = []
136
- for token, count in token_counts.items():
137
- if token in vocab:
138
- indices.append(vocab[token])
139
- values.append(float(count))
140
-
141
- sparse_vectors.append({
142
- 'indices': indices,
143
- 'values': values
144
- })
145
-
146
- return sparse_vectors
147
-
148
- def mmr_select(query_embedding, doc_embeddings, k=6, lambda_mult=0.6):
149
- selected = []
150
- candidates = list(range(len(doc_embeddings)))
151
- doc_embeddings = np.array(doc_embeddings)
152
-
153
- query_embedding = np.array(query_embedding).reshape(1, -1)
154
- query_doc_sims = cosine_similarity(query_embedding, doc_embeddings)[0]
155
-
156
- for _ in range(k):
157
- mmr_score = []
158
- for idx in candidates:
159
- if not selected:
160
- diversity = 0
161
- else:
162
- selected_embeddings = doc_embeddings[selected]
163
- diversity = max(cosine_similarity(
164
- doc_embeddings[idx].reshape(1, -1),
165
- selected_embeddings
166
- )[0])
167
- score = lambda_mult * query_doc_sims[idx] - (1 - lambda_mult) * diversity
168
- mmr_score.append(score)
169
- selected_idx = candidates[np.argmax(mmr_score)]
170
- selected.append(selected_idx)
171
- candidates.remove(selected_idx)
172
-
173
- return selected
174
 
 
175
  @app.post("/api/v1/hackrx/run", response_model=RunResponse, dependencies=[Depends(verify_api_key)])
176
  async def run_hackrx(req: RunRequest):
177
  chunks = load_and_chunk(str(req.documents))
178
  if not chunks:
179
  return JSONResponse({"error": "No documents could be processed."}, status_code=400)
180
 
181
- # Initialize Pinecone index
182
- index_name = "insurance-hybrid-search"
183
- pc = ml_models["pc"]
184
- index = create_serverless_index(pc, index_name)
185
 
186
- # Prepare document texts
187
- doc_texts = [doc.page_content for doc in chunks]
 
 
 
 
188
 
189
- # Generate embeddings using Pinecone's multilingual-e5-large
190
- dense_embeddings = await embed_with_pinecone_inference(pc, doc_texts)
 
 
 
 
 
 
 
 
 
191
 
192
- # Generate sparse vectors
193
- sparse_embeddings = generate_sparse_vectors(doc_texts)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
 
195
- # Upsert to Pinecone with hybrid vectors
196
- vectors_to_upsert = []
197
- for i, (doc, dense_emb, sparse_emb) in enumerate(zip(chunks, dense_embeddings, sparse_embeddings)):
198
- vectors_to_upsert.append({
199
- 'id': f'doc_{i}',
200
- 'values': dense_emb,
201
- 'sparse_values': sparse_emb,
202
- 'metadata': {
203
- 'text': doc.page_content,
204
- 'source': getattr(doc, 'metadata', {}).get('source', 'unknown')
205
- }
206
- })
207
 
208
- # Batch upsert
209
- batch_size = 100
210
- for i in range(0, len(vectors_to_upsert), batch_size):
211
- batch = vectors_to_upsert[i:i+batch_size]
212
- index.upsert(vectors=batch)
213
 
214
- print(f"✅ Upserted {len(vectors_to_upsert)} vectors to Pinecone")
215
-
216
- # Generate question embeddings
217
- question_embeddings = await embed_with_pinecone_inference(pc, req.questions)
218
- question_sparse = generate_sparse_vectors(req.questions)
219
-
 
 
 
 
 
 
 
 
 
 
 
 
220
  async def async_retrieve_and_rerank(question: str, q_idx: int):
221
- # Hybrid search with alpha=0.5 (balanced dense/sparse)
222
- alpha = 0.5
223
-
224
- # Scale embeddings for hybrid search
225
- dense_query = [v * alpha for v in question_embeddings[q_idx]]
226
- sparse_query = {
227
- 'indices': question_sparse[q_idx]['indices'],
228
- 'values': [v * (1 - alpha) for v in question_sparse[q_idx]['values']]
229
- }
230
-
231
- # Query Pinecone with hybrid search
232
- results = index.query(
233
- vector=dense_query,
234
- sparse_vector=sparse_query,
235
- top_k=8,
236
- include_metadata=True
237
- )
238
-
239
- # Extract contexts and embeddings for MMR
240
- contexts = [match['metadata']['text'] for match in results['matches']]
241
- context_embeddings = [match['values'] for match in results['matches']]
242
-
243
- # Apply MMR selection
244
  query_embedding = question_embeddings[q_idx]
245
  selected_indices = mmr_select(
246
- query_embedding=query_embedding,
247
- doc_embeddings=context_embeddings,
248
- k=6,
249
- lambda_mult=0.6,
250
- )
251
-
252
- top_chunks = [contexts[j] for j in selected_indices]
253
  return "\n\n".join(top_chunks)
254
-
255
- # Retrieve and rerank all questions in parallel
256
  retrieved_chunks_all = await asyncio.gather(
257
  *[async_retrieve_and_rerank(q, i) for i, q in enumerate(req.questions)]
258
- )
 
 
259
 
260
- # Generate answers
261
  tasks = []
262
  for i in range(len(req.questions)):
263
  prompt_input = {
@@ -265,16 +236,23 @@ async def run_hackrx(req: RunRequest):
265
  "context": retrieved_chunks_all[i]
266
  }
267
  tasks.append(ml_models["llm"].ainvoke(ml_models["prompt_template"].format_prompt(**prompt_input)))
268
-
269
  results = await asyncio.gather(*tasks)
270
  answers = []
271
 
272
  for msg in results:
 
273
  if hasattr(msg, "content"):
274
  answers.append(msg.content.strip())
 
 
275
 
276
  return JSONResponse({"answers": answers}, status_code=200)
277
 
278
  @app.get("/", include_in_schema=False)
279
  def root():
280
- return {"message": "API is running. Go to /docs for documentation."}
 
 
 
 
 
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
 
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
  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
  "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)